Correcting a single voucher cleared the momsdeklaration's RC_BASIS_MISSING error and the whole per-voucher worklist with it: the check tested mere presence of ruta 20-24 basis, the stepper re-derived its landing step and yanked the user to Granska mid-work, and the remounted checks card never refetched gaps once the aggregate check stopped firing. The declaration then claimed "klart" while the remaining vouchers still under-reported rutor 20-24 (FK004). - Make RC_BASIS_MISSING/RC_OUTPUT_MISSING proportional: compare reported basis against the basis the per-rate output boxes imply (moms/sats), with a 0.5% + 1 kr tolerance for per-voucher ore rounding. - Fetch the rc-basis-gaps worklist once per period, ungated from the aggregate check, so remaining rows survive remounts. - Latch the automatic stepper landing once per period so a refetch after a korrigering cannot navigate the user off Kontrollera. - Resolve rc-basis-gaps against the rakenskapsar (fiscal_period_id) for helarsmoms, matching the declaration totals; a calendar span hid gap vouchers in the tail of an extended first year. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
263 lines
9.1 KiB
TypeScript
263 lines
9.1 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/)
|
|
})
|
|
|
|
// Regression (2026-07-24): the check compared presence, not proportion, so
|
|
// correcting ONE voucher out of ~39 cleared the error while ~51 tkr of
|
|
// basis was still missing and the declaration claimed to be ready.
|
|
it('flags ERROR when RC basis is only partially reported', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta21: 2109.16, // one corrected voucher
|
|
ruta30: 13446.18, // fiktiv moms for ~39 vouchers → expects ~53 785 kr basis
|
|
ruta48: 13446.18,
|
|
ruta49: 0,
|
|
}
|
|
const finding = runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_BASIS_MISSING')
|
|
expect(finding?.status).toBe('ERROR')
|
|
expect(finding?.message).toMatch(/saknas/)
|
|
})
|
|
|
|
it('does not flag RC basis inside the per-voucher rounding tolerance', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta21: 9990, // expected 10000, tolerance max(1, 0.5%) = 50
|
|
ruta30: 2500,
|
|
ruta48: 2500,
|
|
ruta49: 0,
|
|
}
|
|
const findings = runVatDeclarationChecks(rutor)
|
|
expect(findings.find((f) => f.code === 'RC_BASIS_MISSING')).toBeUndefined()
|
|
expect(findings.find((f) => f.code === 'RC_OUTPUT_MISSING')).toBeUndefined()
|
|
})
|
|
|
|
it('flags ERROR when a whole voucher of basis is missing beyond the tolerance', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta21: 9000, // expected 10000: one ~1000 kr voucher missing
|
|
ruta30: 2500,
|
|
ruta48: 2500,
|
|
ruta49: 0,
|
|
}
|
|
expect(
|
|
runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_BASIS_MISSING')?.status,
|
|
).toBe('ERROR')
|
|
})
|
|
|
|
it('flags ERROR when basis exceeds what the output VAT accounts for', () => {
|
|
const rutor: VatDeclarationRutor = {
|
|
...emptyRutor,
|
|
ruta21: 60000, // expected only 10000 from ruta30: fiktiv moms missing
|
|
ruta30: 2500,
|
|
ruta48: 2500,
|
|
ruta49: 0,
|
|
}
|
|
expect(
|
|
runVatDeclarationChecks(rutor).find((f) => f.code === 'RC_OUTPUT_MISSING')?.status,
|
|
).toBe('ERROR')
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|