Files
accounted/lib/vat/__tests__/moms-box-mapping.test.ts
T
MattssonandClaude Opus 4.7 da39eb2d43 Bug/momsdek overflow (#465)
* feat(settings): add option for company name position in invoice PDF

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* feat(migrations): add backfill for VAT account labels to correct bad seed data

* fix(ui): improve accessibility for company name position toggle in PDF settings

* fix(bookkeeping): align BAS 2026 reference data with official PDF

Reconciled lib/bookkeeping/bas-data/ against the BAS 2026 v1.1 official
chart (1286 accounts). All real discrepancies fixed:

- 2089 Fond för utvecklingsutgifter: k2_excluded → true
- 8417 Räntekostnader för dold räntekompensation: k2_excluded → true
- 1250, 1260 renamed to "(Fritt konto för Inventarier, verktyg och
  installationer)" — BAS 2026 freed these slots
- Periodiseringsfond 2120-2139: added year suffixes (2120 = "...2020"
  etc.) and added 8 missing accounts (2121-2127, 2129) for years
  2019, 2021-2027. Dropped phantom 2022/2024 prior-parser garbage.
- 4075-4078: EUland → EU-land
- 8411: förlagsoch → förlags- och

Verified: 1282 of 1286 PDF accounts match exactly after edits (remaining
4 are PDF-parser artifacts, not real data). Build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(migrations): backfill BAS 2026 account labels in chart_of_accounts

Companion to the TS reference fix. Updates existing companies' rows where
they still carry seed-data typos or generic names that don't match BAS 2026:

- 4075-4078: EUland → EU-land (hyphenation)
- 8411: förlagsoch → förlags- och (hyphenation)
- 2120, 2130-2137, 2139: rename "Periodiseringsfond" (generic, no year) to
  the BAS 2026 canonical name with year suffix

Defensive: every WHERE clause matches an EXACT current value. Rows that
have been manually renamed by users — including those with a wrong year
that may reference legacy fonds from an earlier BAS numbering cycle — are
left untouched. No row is deleted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(migrations): fix wrong-year labels on Periodiseringsfond accounts

Follow-up to 20260513140000. The first backfill only renamed accounts whose
name was the generic "Periodiseringsfond" (no year). Many customers were
seeded from an older BAS numbering cycle where 2126 = "2016", 2127 = "2017",
etc. — BAS 2026 reuses those account numbers for years 2026/2027.

This migration aligns the year tag with the BAS 2026 meaning of each
account number across 2120-2127, 2129, 2130-2137, 2139. Only rows whose
name still starts with "Periodiseringsfond" are touched — customers who
renamed the account to something custom keep their name.

Verified on staging: all 18 accounts now carry BAS 2026 canonical names.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(data): update account names and descriptions for clarity and consistency

* fix(migrations): backfill account names for BAS 2026 freed accounts and refine Periodiseringsfond name matching

* feat: enhance VAT handling with reverse charge logic and supplier type support

* feat: implement VAT declaration validation rules and enhance moms box mapping

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:17:32 +02:00

156 lines
5.9 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 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([])
})
})