From cf373e9dd165eb45e038765968420a9b1cb8e3d1 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Sat, 8 Aug 2026 10:40:36 +0200 Subject: [PATCH] =?UTF-8?q?fix(vat):=20make=20ruta=2041=20reachable=20(omv?= =?UTF-8?q?=C3=A4nd=20skattskyldighet=20sales,=203231-3233)=20(#1456)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * docs: record the ruta 41 static-mapping decision Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- DECISIONS.md | 1 + lib/reports/__tests__/vat-declaration.test.ts | 25 +++++++++++++++++++ lib/reports/vat-declaration.ts | 6 +++++ lib/reports/vat-revenue-accounts.ts | 8 +++--- lib/vat/__tests__/moms-box-mapping.test.ts | 6 +++++ lib/vat/moms-box-mapping.ts | 5 ++++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 4cf4b650..6e65fa36 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -826,3 +826,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-06] Review-workflow triage on the Bucket A branch (13 confirmed findings): fixed 10, incl. the branch-killing one (setActiveCompany's cookie write throws in Server Component render, so the /select-company auto-forward silently never fired: the cookie set is now best-effort because the gnubok-company-id cookie is write-only compat nothing reads). Batch "Ingen moms" now goes over the wire as 'exempt' instead of collapsing to undefined, which had an explicit no-VAT choice booking the derived 25%; the same pre-existing collapse in QuickReviewDialog/CategoryExpandedDialog is left for a follow-up. Skipped by choice: generalizing AiFilledIndicator for history provenance (the note's copy already names the source) and converting BulkBookInboxDialog's hardcoded-Swedish option lists to i18n (whole-file migration, not this branch's divergence). Monthly momsdeklaration default is deadline-aware (M-2 until the 12th/17th, M-1 after; over-40M always M-1) mirroring deadline-config, not just calendar-ended. [2026-08-07] Sandbox cleanup repaired via a dedicated gnubok.sandbox_cleanup transaction-local flag (respected by write_audit_log, audit_log_immutable, enforce_dimension_registry_guards, enforce_pending_operations_no_delete) instead of tombstoning sandbox users like the real account-deletion flow: sandbox data is synthetic demo content, not rakenskapsinformation, so full deletion is the correct GDPR/BFL posture and the audit-log DELETE bypass re-verifies per row that the company is a sandbox before letting anything through. Also revoked the default PUBLIC EXECUTE both cleanup RPCs had carried on prod since March (anon could call them via PostgREST), and validated the whole delete chain by replaying it against prod inside aborted transactions (21 sampled users across all seed eras) plus a committed staging run. [2026-08-07] QuickReviewDialog's 'none'-collapse closed with a deviation test, not a blanket map: 'none' as the SEEDED default (exempt categories) stays off the wire so untouched bank-fee bookings remain byte-identical, while 'none' as a user deviation goes as explicit 'exempt' (books no VAT line, records the classification the momsdeklaration should see, lands income on 3004). The batch dialog's blanket 'none'→'exempt' map from #1443 is correct THERE because its select has no seeded-per-category default. CategoryExpandedDialog carries the same collapse but has zero references (dead code): left untouched, deletion is hygiene for a separate sweep. +[2026-08-08] Ruta 41 unblocked with a static ACCOUNT_RUTA mapping of 3231/3232/3233 (incl. 3232/3233, in the SKV 4700 mapping though not in our shipped BAS 2026 catalog) instead of starting the per-account VAT-treatment picker: the ElvaSmultron/Spiris support case needed omvänd-moms sales in the declaration now, and RC sales deliberately stay out of the ruta 05-08 vs 10-12 pairing checks because the buyer accounts for the VAT. The picker (any custom konto to any ruta, incl. purchase-side 4056-style Visma accounts to ruta 20) is #1457. diff --git a/lib/reports/__tests__/vat-declaration.test.ts b/lib/reports/__tests__/vat-declaration.test.ts index e36a0b1a..eae78ed7 100644 --- a/lib/reports/__tests__/vat-declaration.test.ts +++ b/lib/reports/__tests__/vat-declaration.test.ts @@ -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) diff --git a/lib/reports/vat-declaration.ts b/lib/reports/vat-declaration.ts index babe375d..49b2d74a 100644 --- a/lib/reports/vat-declaration.ts +++ b/lib/reports/vat-declaration.ts @@ -108,6 +108,12 @@ export const ACCOUNT_RUTA: Record { 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') diff --git a/lib/vat/moms-box-mapping.ts b/lib/vat/moms-box-mapping.ts index 8be3f54a..41627443 100644 --- a/lib/vat/moms-box-mapping.ts +++ b/lib/vat/moms-box-mapping.ts @@ -82,6 +82,11 @@ export const ACCOUNT_TO_BOX: Record = { // 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)