Files
accounted/lib/reports/__tests__/vat-declaration-checks.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

208 lines
7.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import type { VatDeclarationRutor } from '@/types'
import { runVatDeclarationChecks } from '../vat-declaration-checks'
const emptyRutor: VatDeclarationRutor = {
ruta05: 0, ruta06: 0, ruta07: 0, ruta08: 0,
ruta10: 0, ruta11: 0, ruta12: 0,
ruta20: 0, ruta21: 0, ruta22: 0, ruta23: 0, ruta24: 0,
ruta30: 0, ruta31: 0, ruta32: 0,
ruta35: 0, ruta36: 0, ruta37: 0, ruta38: 0,
ruta39: 0, ruta40: 0, ruta41: 0, ruta42: 0,
ruta48: 0, ruta49: 0,
ruta50: 0, ruta60: 0, ruta61: 0, ruta62: 0,
}
describe('runVatDeclarationChecks', () => {
it('returns empty findings for a balanced sales-only declaration', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 100000,
ruta10: 25000,
ruta49: 25000,
}
expect(runVatDeclarationChecks(rutor)).toEqual([])
})
it('returns empty findings for a balanced declaration with RC basis + output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta21: 10000, // EU services basis
ruta30: 2500, // RC output VAT
ruta48: 2500, // matching input VAT
ruta49: 0,
}
expect(runVatDeclarationChecks(rutor)).toEqual([])
})
// FK004 mirror: SKV's primary rejection signal we want to catch locally.
it('flags ERROR when ruta 30-32 populated but ruta 20-24 is empty', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 78852,
ruta10: 19713,
ruta30: 2500,
ruta48: 2500,
ruta49: 19713,
}
const findings = runVatDeclarationChecks(rutor)
const fk004 = findings.find((f) => f.code === 'RC_BASIS_MISSING')
expect(fk004).toBeDefined()
expect(fk004?.status).toBe('ERROR')
expect(fk004?.message).toMatch(/ruta 30-32/)
expect(fk004?.message).toMatch(/ruta 20-24/)
})
it('flags ERROR when basis is present but no output RC VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta21: 10000,
ruta48: 2500,
ruta49: -2500,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'RC_OUTPUT_MISSING')?.status).toBe('ERROR')
})
it('warns when input VAT is materially smaller than RC output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta21: 10000,
ruta30: 2500,
ruta48: 100, // Calculated input VAT missing — should be ~2500
ruta49: 2400,
}
const findings = runVatDeclarationChecks(rutor)
const mismatch = findings.find((f) => f.code === 'RC_INPUT_VAT_MISMATCH')
expect(mismatch?.status).toBe('WARNING')
})
// FK009 detection: if our calculator and SKV's recomputed sum disagree
// we flag locally so we never submit a drift.
it('flags ERROR when ruta49 drifts from the canonical formula', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta10: 100,
ruta48: 20,
ruta49: 99, // wrong — should be 80
}
const findings = runVatDeclarationChecks(rutor)
const drift = findings.find((f) => f.code === 'SUMMA_MOMS_DRIFT')
expect(drift?.status).toBe('ERROR')
})
it('ignores fractional-öre drift (≤ 0.5 SEK)', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta10: 100.30,
ruta48: 20.10,
ruta49: 80.20, // canonical formula exactly, only fractional öre
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'SUMMA_MOMS_DRIFT')).toBeUndefined()
})
// SKV §4.1.1.4 rule 1 — taxable sales base without output VAT.
it('flags ERROR when taxable sales (ruta 05) booked without output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 10000,
// ruta 10/11/12 all zero — SKV rule 1 violation
ruta49: 0,
}
const findings = runVatDeclarationChecks(rutor)
const finding = findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')
expect(finding?.status).toBe('ERROR')
expect(finding?.message).toMatch(/försäljning/)
expect(finding?.message).toMatch(/utgående moms/)
})
it('flags ERROR for ruta 06 (uttag) without output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta06: 5000,
ruta49: 0,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')?.status).toBe('ERROR')
})
it('does not flag taxable sales without output VAT when output VAT is present', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 10000,
ruta10: 2500,
ruta49: 2500,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')).toBeUndefined()
})
// Mirror: output VAT without taxable sales base.
it('flags ERROR when output VAT booked without taxable sales base', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
// No ruta 05/06/07/08
ruta10: 2500,
ruta49: 2500,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'OUTPUT_VAT_WITHOUT_SALES_BASE')?.status).toBe('ERROR')
})
// SKV §4.1.1.4 rule 5 — import base without import output VAT.
it('flags ERROR when import base (ruta 50) without import output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta50: 10000,
// ruta 60/61/62 all zero
ruta48: 0,
ruta49: 0,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'IMPORT_BASE_WITHOUT_OUTPUT')?.status).toBe('ERROR')
})
// SKV §4.1.1.4 rule 6 — import output VAT without import base.
it('flags ERROR when import output VAT (ruta 60) without ruta 50', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta60: 2500,
ruta48: 2500,
ruta49: 0,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'IMPORT_OUTPUT_WITHOUT_BASE')?.status).toBe('ERROR')
})
it('does not flag import checks when both base and output VAT are present', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta50: 10000,
ruta60: 2500,
ruta48: 2500,
ruta49: 0,
}
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'IMPORT_BASE_WITHOUT_OUTPUT')).toBeUndefined()
expect(findings.find((f) => f.code === 'IMPORT_OUTPUT_WITHOUT_BASE')).toBeUndefined()
})
// Multiple findings should surface together so the user sees the whole picture.
it('reports multiple distinct findings for a deeply broken declaration', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 10000, // taxable sales but no output VAT
ruta30: 2500, // RC output but no RC basis
ruta50: 5000, // import base but no import output
ruta48: 0,
ruta49: 2500,
}
const findings = runVatDeclarationChecks(rutor)
const codes = findings.map((f) => f.code).sort()
expect(codes).toContain('TAXABLE_SALES_WITHOUT_OUTPUT')
expect(codes).toContain('RC_BASIS_MISSING')
expect(codes).toContain('IMPORT_BASE_WITHOUT_OUTPUT')
})
})