fix(vat): make ruta 41 reachable (omvänd skattskyldighet sales, 3231-3233) (#1456)

* fix(vat): map 3231-3233 to ruta 41 in the momsdeklaration

Ruta 41 (försäljning när köparen är betalningsskyldig i Sverige) existed
in the type, the eSKD file and the Skatteverket mapper, but no account
could ever reach it: 3231-3233 were deliberately parked in
RUTA_05_EXCLUDED_ACCOUNTS, so byggmoms/omvänd-skattskyldighet sales
vanished from the declaration entirely. Map them statically in
ACCOUNT_RUTA and ACCOUNT_TO_BOX; the ACCOUNT_TO_BOX guard now keeps them
out of the dynamic ruta 05 set instead of the exclusion list. RC sales
carry no output VAT, so they stay out of the ruta 05-08 vs 10-12
pairing checks, pinned by test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: record the ruta 41 static-mapping decision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-08 10:40:36 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent c912fc5633
commit cf373e9dd1
6 changed files with 48 additions and 3 deletions
@@ -98,6 +98,7 @@ import {
getVatDeclarationSummary,
calculateVatDeclaration,
rcInputTotalsFromDeclaration,
rutorFromTotals,
} from '../vat-declaration'
import { runVatDeclarationChecks } from '../vat-declaration-checks'
import type { VatDeclaration } from '@/types'
@@ -116,6 +117,30 @@ beforeEach(() => {
// Pure function tests: no mocks needed
// ============================================================
describe('rutorFromTotals: ruta 41 (omvänd skattskyldighet, sales side)', () => {
it('projects 3231/3232/3233 credit balances into ruta 41', () => {
const totals = new Map([
['3231', { debit: 0, credit: 100_000 }],
['3232', { debit: 500, credit: 10_500 }],
['3233', { debit: 0, credit: 0 }],
])
const rutor = rutorFromTotals(totals)
expect(rutor.ruta41).toBe(110_000)
// Buyer accounts for the VAT: an RC sale must not leak into the
// taxable-sales pairing (rutor 05-08) nor into the net (ruta 49).
expect(rutor.ruta05).toBe(0)
expect(rutor.ruta49).toBe(0)
})
it('a pure ruta 41 declaration passes the sales/output pairing checks', () => {
const totals = new Map([['3231', { debit: 0, credit: 50_000 }]])
const rutor = rutorFromTotals(totals)
const findings = runVatDeclarationChecks(rutor)
expect(findings.map((f) => f.code)).not.toContain('TAXABLE_SALES_WITHOUT_OUTPUT')
expect(findings.map((f) => f.code)).not.toContain('OUTPUT_VAT_WITHOUT_SALES_BASE')
})
})
describe('calculatePeriodDates', () => {
it('returns correct dates for monthly period', () => {
const { start, end } = calculatePeriodDates('monthly', 2024, 1)
+6
View File
@@ -108,6 +108,12 @@ export const ACCOUNT_RUTA: Record<string, { box: keyof VatDeclarationRutor; side
'3404': { box: 'ruta42', side: 'credit' }, // Momsfria uttag
'3980': { box: 'ruta42', side: 'credit' }, // Erhållna offentliga stöd m.m.
'3994': { box: 'ruta42', side: 'credit' }, // Övriga rörelseintäkter momsfria
// Revenue: omvänd skattskyldighet inom Sverige → ruta 41. The seller books
// NO output VAT (the buyer accounts for it via rutor 23-24/30-32), so these
// deliberately stay OUT of the ruta 05-08 vs 10-12 pairing checks.
'3231': { box: 'ruta41', side: 'credit' }, // Försäljning byggsektorn, omvänd betalningsskyldighet
'3232': { box: 'ruta41', side: 'credit' }, // Omvänd betalningsskyldighet, övriga (skrot m.m.)
'3233': { box: 'ruta41', side: 'credit' }, // Omvänd betalningsskyldighet, övriga
// Reverse-charge purchase bases (debit on cost accounts) → ruta 20-24, 50
'4515': { box: 'ruta20', side: 'debit' }, // Inköp varor EU 25%
'4516': { box: 'ruta20', side: 'debit' }, // Inköp varor EU 12%
+5 -3
View File
@@ -30,17 +30,19 @@ import { ACCOUNT_TO_BOX } from '@/lib/vat/moms-box-mapping'
* .claude/skills/swedish-vat/references/vat-compliance-reference.md §7:
*
* 3211/3212/3220 → ruta 07 (vinstmarginalbeskattning)
* 3231/3232/3233 → ruta 41 (försäljning där köparen är betalningsskyldig)
* 3913 → ruta 08 (hyresinkomster, frivillig skattskyldighet)
*
* Rutor 07, 08 and 41 are not mappable yet (see the note in
* Rutor 07 and 08 are not mappable yet (see the note in
* vat-declaration-checks.ts). Until they are, these accounts stay out of the
* declaration entirely: that understates one ruta, whereas sweeping them into
* ruta 05 would file the amount in the wrong box.
*
* 3231/3232/3233 (ruta 41, omvänd skattskyldighet) used to sit here for the
* same reason but are now statically mapped in ACCOUNT_RUTA/ACCOUNT_TO_BOX,
* so the ACCOUNT_TO_BOX guard below excludes them from ruta 05.
*/
export const RUTA_05_EXCLUDED_ACCOUNTS = new Set([
'3211', '3212', '3220',
'3231', '3232', '3233',
'3913',
])
@@ -26,6 +26,12 @@ describe('ACCOUNT_TO_BOX', () => {
expect(ACCOUNT_TO_BOX['3305']).toBe('40')
})
it('maps domestic reverse-charge sales accounts to ruta 41', () => {
expect(ACCOUNT_TO_BOX['3231']).toBe('41')
expect(ACCOUNT_TO_BOX['3232']).toBe('41')
expect(ACCOUNT_TO_BOX['3233']).toBe('41')
})
it('maps all output VAT accounts including parent/summary and vilande', () => {
expect(ACCOUNT_TO_BOX['2610']).toBe('10')
expect(ACCOUNT_TO_BOX['2611']).toBe('10')
+5
View File
@@ -82,6 +82,11 @@ export const ACCOUNT_TO_BOX: Record<string, MomsBox> = {
// Non-EU services → Box 40
'3305': '40', // Försäljning tjänster export utanför EU
// Domestic reverse-charge sales (buyer liable for VAT) → Box 41
'3231': '41', // Försäljning byggsektorn, omvänd betalningsskyldighet
'3232': '41', // Omvänd betalningsskyldighet, övriga (skrot m.m.)
'3233': '41', // Omvänd betalningsskyldighet, övriga
// VAT-exempt sales → Box 42
'3004': '42', // Momsfri försäljning (AB)
'3100': '42', // Momsfria intäkter (EF)