Files
accounted/lib/vat/__tests__/moms-box-mapping.test.ts
T
cf373e9dd1 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>
2026-08-08 10:40:36 +02:00

162 lines
6.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
ACCOUNT_TO_BOX,
BOX_LABELS,
getBoxForAccount,
getBoxLabel,
type MomsBox,
} from '../moms-box-mapping'
import { ACCOUNT_RUTA } from '@/lib/reports/vat-declaration'
describe('ACCOUNT_TO_BOX', () => {
it('has a label for every box ID used in the map', () => {
const usedBoxes = new Set(Object.values(ACCOUNT_TO_BOX))
for (const box of usedBoxes) {
expect(BOX_LABELS[box]).toBeTruthy()
}
})
it('maps all known revenue accounts to a sales box', () => {
expect(ACCOUNT_TO_BOX['3001']).toBe('05')
expect(ACCOUNT_TO_BOX['3002']).toBe('05')
expect(ACCOUNT_TO_BOX['3003']).toBe('05')
expect(ACCOUNT_TO_BOX['3108']).toBe('35')
expect(ACCOUNT_TO_BOX['3308']).toBe('39')
expect(ACCOUNT_TO_BOX['3105']).toBe('36')
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')
expect(ACCOUNT_TO_BOX['2618']).toBe('10')
expect(ACCOUNT_TO_BOX['2620']).toBe('11')
expect(ACCOUNT_TO_BOX['2630']).toBe('12')
expect(ACCOUNT_TO_BOX['2614']).toBe('30')
expect(ACCOUNT_TO_BOX['2624']).toBe('31')
expect(ACCOUNT_TO_BOX['2634']).toBe('32')
})
it('maps all input VAT accounts including parent and domestic RC', () => {
expect(ACCOUNT_TO_BOX['2640']).toBe('48')
expect(ACCOUNT_TO_BOX['2641']).toBe('48')
expect(ACCOUNT_TO_BOX['2645']).toBe('48')
expect(ACCOUNT_TO_BOX['2647']).toBe('48')
expect(ACCOUNT_TO_BOX['2649']).toBe('48')
})
it('maps reverse-charge basis accounts to the correct ruta', () => {
// EU goods → ruta 20
expect(ACCOUNT_TO_BOX['4515']).toBe('20')
expect(ACCOUNT_TO_BOX['4516']).toBe('20')
expect(ACCOUNT_TO_BOX['4517']).toBe('20')
// EU services → ruta 21
expect(ACCOUNT_TO_BOX['4535']).toBe('21')
expect(ACCOUNT_TO_BOX['4536']).toBe('21')
expect(ACCOUNT_TO_BOX['4537']).toBe('21')
// Non-EU services → ruta 22
expect(ACCOUNT_TO_BOX['4531']).toBe('22')
expect(ACCOUNT_TO_BOX['4532']).toBe('22')
expect(ACCOUNT_TO_BOX['4533']).toBe('22')
// Domestic goods RC → ruta 23
expect(ACCOUNT_TO_BOX['4415']).toBe('23')
expect(ACCOUNT_TO_BOX['4416']).toBe('23')
expect(ACCOUNT_TO_BOX['4417']).toBe('23')
// Domestic services RC → ruta 24
expect(ACCOUNT_TO_BOX['4425']).toBe('24')
expect(ACCOUNT_TO_BOX['4426']).toBe('24')
expect(ACCOUNT_TO_BOX['4427']).toBe('24')
})
it('maps import beskattningsunderlag accounts to ruta 50', () => {
expect(ACCOUNT_TO_BOX['4545']).toBe('50')
expect(ACCOUNT_TO_BOX['4546']).toBe('50')
expect(ACCOUNT_TO_BOX['4547']).toBe('50')
})
it('maps import output VAT accounts to ruta 60/61/62', () => {
expect(ACCOUNT_TO_BOX['2615']).toBe('60')
expect(ACCOUNT_TO_BOX['2625']).toBe('61')
expect(ACCOUNT_TO_BOX['2635']).toBe('62')
})
it('maps momspliktiga uttag accounts to ruta 06', () => {
expect(ACCOUNT_TO_BOX['3401']).toBe('06')
expect(ACCOUNT_TO_BOX['3402']).toBe('06')
expect(ACCOUNT_TO_BOX['3403']).toBe('06')
})
})
describe('getBoxForAccount', () => {
it('returns the box for known accounts', () => {
expect(getBoxForAccount('2611')).toBe('10')
expect(getBoxForAccount('4535')).toBe('21')
})
it('returns undefined for unknown accounts', () => {
expect(getBoxForAccount('9999')).toBeUndefined()
expect(getBoxForAccount('1930')).toBeUndefined() // bank account, not VAT-related
})
})
describe('getBoxLabel', () => {
it('returns Swedish labels for every box', () => {
expect(getBoxLabel('10')).toMatch(/Utgående moms 25%/)
expect(getBoxLabel('30')).toMatch(/inköp 25%/)
expect(getBoxLabel('48')).toMatch(/Ingående moms/)
expect(getBoxLabel('49')).toMatch(/Moms att betala/)
})
})
// Regression guard: ACCOUNT_TO_BOX must stay aligned with the source-of-truth
// mapping in vat-declaration.ts. If a new account is added to one map without
// the other, the calculation and the cross-validation labels drift apart.
describe('ACCOUNT_TO_BOX ↔ ACCOUNT_RUTA alignment', () => {
const RUTA_TO_BOX: Record<string, MomsBox> = {
ruta05: '05', ruta06: '06', ruta07: '07', ruta08: '08',
ruta10: '10', ruta11: '11', ruta12: '12',
ruta20: '20', ruta21: '21', ruta22: '22', ruta23: '23', ruta24: '24',
ruta30: '30', ruta31: '31', ruta32: '32',
ruta35: '35', ruta36: '36', ruta37: '37', ruta38: '38',
ruta39: '39', ruta40: '40', ruta41: '41', ruta42: '42',
ruta48: '48', ruta49: '49',
ruta50: '50', ruta60: '60', ruta61: '61', ruta62: '62',
}
it('every account in ACCOUNT_RUTA exists in ACCOUNT_TO_BOX with the matching box', () => {
const drift: string[] = []
for (const [account, mapping] of Object.entries(ACCOUNT_RUTA)) {
const expectedBox = RUTA_TO_BOX[mapping.box]
const actualBox = ACCOUNT_TO_BOX[account]
if (!actualBox) {
drift.push(`missing in ACCOUNT_TO_BOX: ${account} (should be box ${expectedBox})`)
} else if (actualBox !== expectedBox) {
drift.push(`mismatched box for ${account}: ACCOUNT_TO_BOX=${actualBox}, ACCOUNT_RUTA=${expectedBox}`)
}
}
expect(drift).toEqual([])
})
it('every account in ACCOUNT_TO_BOX exists in ACCOUNT_RUTA (or is an extra cross-validation hint)', () => {
// Allowed extras: accounts in ACCOUNT_TO_BOX that don't feed the declaration
// but are useful for the Export VAT Monitor / EU Sales List. Currently these
// are the frakter accounts that follow goods treatment.
const allowedExtras = new Set<string>(['3521', '3522', '3109'])
const drift: string[] = []
for (const account of Object.keys(ACCOUNT_TO_BOX)) {
if (allowedExtras.has(account)) continue
if (!ACCOUNT_RUTA[account]) {
drift.push(`extra in ACCOUNT_TO_BOX: ${account} (not in ACCOUNT_RUTA: consider adding to declaration mapping or to allowedExtras)`)
}
}
expect(drift).toEqual([])
})
})