From 6c79f216795fb49e6a15afbf3f3d2eb2dcb32a85 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:04:48 +0200 Subject: [PATCH] fix: INK2 rounding tolerance and clearer warnings (#197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add rounding tolerance to INK2 balance check and clarify warnings INK2/SRU rounds each ruta independently to whole kronor, so with 11+ rutor the accumulated rounding can produce a 1-2 kr difference that triggered a false "balance sheet not in balance" warning. Add a 2 kr tolerance. Also clarify the unclosed fiscal year warning to indicate that generation still works. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: also warn when equity/liabilities exist but assets are zero Address Greptile review feedback — the balance check guard should trigger when either side has a non-zero total, not only when assets > 0. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .../ink2/__tests__/ink2-engine.test.ts | 42 ++++++++++++++++++- lib/reports/ink2/ink2-engine.ts | 24 ++++++++--- lib/reports/ne-bilaga/ne-engine.ts | 2 +- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/lib/reports/ink2/__tests__/ink2-engine.test.ts b/lib/reports/ink2/__tests__/ink2-engine.test.ts index c31801fb..29c7f46a 100644 --- a/lib/reports/ink2/__tests__/ink2-engine.test.ts +++ b/lib/reports/ink2/__tests__/ink2-engine.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { INK2_ACCOUNT_MAPPINGS, isAccountInMapping } from '../ink2-engine' +import { INK2_ACCOUNT_MAPPINGS, isAccountInMapping, checkBalanceWarning } from '../ink2-engine' import type { INK2SRUCode } from '../types' /** @@ -255,3 +255,43 @@ describe('INK2 Account Mappings', () => { }) }) }) + +describe('checkBalanceWarning', () => { + it('returns null when perfectly balanced', () => { + expect(checkBalanceWarning(100000, 100000)).toBeNull() + }) + + it('returns null for 1 kr difference (within rounding tolerance)', () => { + expect(checkBalanceWarning(100000, 100001)).toBeNull() + expect(checkBalanceWarning(100001, 100000)).toBeNull() + }) + + it('returns null for 2 kr difference (within rounding tolerance)', () => { + expect(checkBalanceWarning(100000, 100002)).toBeNull() + expect(checkBalanceWarning(100002, 100000)).toBeNull() + }) + + it('returns warning for 3 kr difference (exceeds tolerance)', () => { + expect(checkBalanceWarning(100000, 100003)).not.toBeNull() + expect(checkBalanceWarning(100003, 100000)).not.toBeNull() + }) + + it('returns null when totals are zero', () => { + expect(checkBalanceWarning(0, 0)).toBeNull() + }) + + it('returns null when both totals are zero (no data)', () => { + expect(checkBalanceWarning(0, 0)).toBeNull() + }) + + it('returns warning when assets are zero but equity/liabilities exist', () => { + expect(checkBalanceWarning(0, 5)).not.toBeNull() + }) + + it('includes amounts in warning message', () => { + const warning = checkBalanceWarning(100000, 100005) + expect(warning).toContain('100000') + expect(warning).toContain('100005') + expect(warning).toContain('5') + }) +}) diff --git a/lib/reports/ink2/ink2-engine.ts b/lib/reports/ink2/ink2-engine.ts index 56bb44fb..a775f9ba 100644 --- a/lib/reports/ink2/ink2-engine.ts +++ b/lib/reports/ink2/ink2-engine.ts @@ -194,6 +194,20 @@ function roundToKrona(value: number): number { return Math.round(value) } +/** + * Check if the balance sheet totals differ beyond the expected rounding tolerance. + * Each ruta is independently rounded to whole kronor for SRU output, so with 11+ + * rutor the accumulated rounding can produce a 1-2 kr difference. + */ +export function checkBalanceWarning(totalAssets: number, totalEquityLiabilities: number): string | null { + const balanceDiff = Math.abs(totalAssets - totalEquityLiabilities) + const ROUNDING_TOLERANCE_KR = 2 + if (balanceDiff > ROUNDING_TOLERANCE_KR && (totalAssets > 0 || totalEquityLiabilities > 0)) { + return `Balansräkningen är inte i balans. Tillgångar: ${totalAssets} kr, Eget kapital och skulder: ${totalEquityLiabilities} kr (differens: ${balanceDiff} kr).` + } + return null +} + /** * Generate INK2 declaration for a fiscal period */ @@ -349,18 +363,16 @@ export async function generateINK2Declaration( // Add warnings if (!(period as FiscalPeriod).is_closed) { - warnings.push('Räkenskapsåret är inte stängt. Siffrorna kan ändras.') + warnings.push('Räkenskapsåret är inte stängt — deklarationen kan genereras, men siffrorna kan ändras om fler bokföringar görs.') } if (totalAssets === 0 && totalEquityLiabilities === 0 && rutor['7310'] === 0) { warnings.push('Inga bokförda transaktioner hittades för perioden.') } - const balanceDiff = Math.abs(totalAssets - totalEquityLiabilities) - if (balanceDiff > 0 && totalAssets > 0) { - warnings.push( - `Balansräkningen är inte i balans. Tillgångar: ${totalAssets} kr, Eget kapital och skulder: ${totalEquityLiabilities} kr (differens: ${balanceDiff} kr).` - ) + const balanceWarning = checkBalanceWarning(totalAssets, totalEquityLiabilities) + if (balanceWarning) { + warnings.push(balanceWarning) } return { diff --git a/lib/reports/ne-bilaga/ne-engine.ts b/lib/reports/ne-bilaga/ne-engine.ts index b5aa4b51..e4e16f83 100644 --- a/lib/reports/ne-bilaga/ne-engine.ts +++ b/lib/reports/ne-bilaga/ne-engine.ts @@ -302,7 +302,7 @@ export async function generateNEDeclaration( // Add warnings if (!(period as FiscalPeriod).is_closed) { - warnings.push('Räkenskapsåret är inte stängt. Siffrorna kan ändras.') + warnings.push('Räkenskapsåret är inte stängt — deklarationen kan genereras, men siffrorna kan ändras om fler bokföringar görs.') } if (rutor.R11 === 0 && totalRevenue === 0) {