Files
accounted/lib/reports/ink2/__tests__/ink2-engine.test.ts
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

359 lines
14 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { INK2R_ACCOUNT_MAPPINGS, isAccountInMapping, checkBalanceWarning } from '../ink2-engine'
import type { INK2RSRUCode } from '../types'
/**
* Helper to find which SRU code an account maps to
*/
function findSRUCodeForAccount(accountNumber: string): INK2RSRUCode | null {
for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
if (isAccountInMapping(accountNumber, mapping)) {
return mapping.sruCode
}
}
return null
}
describe('INK2R Account Mappings', () => {
describe('completeness', () => {
it('has mappings covering all INK2R balance sheet and income statement fields', () => {
// 26 asset + 21 equity/liability + 20 income statement = 67 mappings
expect(INK2R_ACCOUNT_MAPPINGS.length).toBeGreaterThanOrEqual(60)
})
it('covers all expected SRU codes', () => {
const codes = new Set(INK2R_ACCOUNT_MAPPINGS.map(m => m.sruCode))
// Balance sheet asset codes
for (const code of ['7201', '7202', '7214', '7215', '7216', '7217', '7281']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
// Equity/liability codes
for (const code of ['7301', '7302', '7321', '7322', '7365', '7368', '7370']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
// Income statement codes
for (const code of ['7410', '7513', '7514', '7515', '7522', '7528']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
})
})
describe('Balance sheet - Assets (per bas.se/kontoplaner/sru/)', () => {
it('1010-1079, 1090-1099 -> 7201 (Immateriella AT excl. förskott)', () => {
expect(findSRUCodeForAccount('1010')).toBe('7201')
expect(findSRUCodeForAccount('1050')).toBe('7201')
expect(findSRUCodeForAccount('1079')).toBe('7201')
expect(findSRUCodeForAccount('1090')).toBe('7201')
expect(findSRUCodeForAccount('1099')).toBe('7201')
})
it('1080-1089 -> 7202 (Förskott immateriella)', () => {
expect(findSRUCodeForAccount('1080')).toBe('7202')
expect(findSRUCodeForAccount('1089')).toBe('7202')
})
it('1100-1119, 1130-1179, 1190-1199 -> 7214 (Byggnader och mark)', () => {
expect(findSRUCodeForAccount('1100')).toBe('7214')
expect(findSRUCodeForAccount('1110')).toBe('7214')
expect(findSRUCodeForAccount('1130')).toBe('7214')
expect(findSRUCodeForAccount('1190')).toBe('7214')
})
it('1120-1129 -> 7216 (Förbättringsutgifter annans fastighet)', () => {
expect(findSRUCodeForAccount('1120')).toBe('7216')
expect(findSRUCodeForAccount('1129')).toBe('7216')
})
it('1180-1189 -> 7217 (Pågående nyanläggningar)', () => {
expect(findSRUCodeForAccount('1180')).toBe('7217')
expect(findSRUCodeForAccount('1189')).toBe('7217')
})
it('1200-1299 -> 7215 (Maskiner och inventarier)', () => {
expect(findSRUCodeForAccount('1200')).toBe('7215')
expect(findSRUCodeForAccount('1250')).toBe('7215')
expect(findSRUCodeForAccount('1299')).toBe('7215')
})
it('1500-1519 -> 7251 (Kundfordringar)', () => {
expect(findSRUCodeForAccount('1500')).toBe('7251')
expect(findSRUCodeForAccount('1510')).toBe('7251')
expect(findSRUCodeForAccount('1519')).toBe('7251')
})
it('1520-1559 -> 7261 (Övriga fordringar, not 7251)', () => {
expect(findSRUCodeForAccount('1520')).toBe('7261')
expect(findSRUCodeForAccount('1550')).toBe('7261')
})
it('1700-1799 -> 7263 (Förutbetalda kostnader)', () => {
expect(findSRUCodeForAccount('1700')).toBe('7263')
expect(findSRUCodeForAccount('1790')).toBe('7263')
})
it('1900-1999 -> 7281 (Kassa och bank)', () => {
expect(findSRUCodeForAccount('1900')).toBe('7281')
expect(findSRUCodeForAccount('1930')).toBe('7281')
expect(findSRUCodeForAccount('1999')).toBe('7281')
})
})
describe('Balance sheet - Equity & Liabilities', () => {
it('2010-2089 -> 7301 (Bundet EK)', () => {
expect(findSRUCodeForAccount('2010')).toBe('7301')
expect(findSRUCodeForAccount('2081')).toBe('7301')
expect(findSRUCodeForAccount('2089')).toBe('7301')
})
it('2090-2099 -> 7302 (Fritt EK)', () => {
expect(findSRUCodeForAccount('2090')).toBe('7302')
expect(findSRUCodeForAccount('2099')).toBe('7302')
})
it('2110-2129 -> 7321 (Periodiseringsfonder)', () => {
expect(findSRUCodeForAccount('2110')).toBe('7321')
expect(findSRUCodeForAccount('2120')).toBe('7321')
})
it('2150-2159 -> 7322 (Ackumulerade överavskrivningar)', () => {
expect(findSRUCodeForAccount('2150')).toBe('7322')
expect(findSRUCodeForAccount('2159')).toBe('7322')
})
it('2440-2449 -> 7365 (Leverantörsskulder)', () => {
expect(findSRUCodeForAccount('2440')).toBe('7365')
expect(findSRUCodeForAccount('2449')).toBe('7365')
})
it('2500-2599 -> 7368 (Skatteskulder)', () => {
expect(findSRUCodeForAccount('2500')).toBe('7368')
expect(findSRUCodeForAccount('2510')).toBe('7368')
})
it('2600-2799 -> 7369 (Övriga skulder kortfristiga, e.g. moms)', () => {
expect(findSRUCodeForAccount('2611')).toBe('7369')
expect(findSRUCodeForAccount('2710')).toBe('7369')
})
it('2900-2999 -> 7370 (Upplupna kostnader)', () => {
expect(findSRUCodeForAccount('2900')).toBe('7370')
expect(findSRUCodeForAccount('2999')).toBe('7370')
})
})
describe('Income statement (per bas.se: CRITICAL: 5000-6999 ALL → 7513)', () => {
it('3000-3799 -> 7410 (Nettoomsättning)', () => {
expect(findSRUCodeForAccount('3000')).toBe('7410')
expect(findSRUCodeForAccount('3001')).toBe('7410')
expect(findSRUCodeForAccount('3100')).toBe('7410')
expect(findSRUCodeForAccount('3799')).toBe('7410')
})
it('3900-3999 -> 7413 (Övriga rörelseintäkter)', () => {
expect(findSRUCodeForAccount('3900')).toBe('7413')
expect(findSRUCodeForAccount('3999')).toBe('7413')
})
it('4000-4499 -> 7511 (Råvaror)', () => {
expect(findSRUCodeForAccount('4000')).toBe('7511')
expect(findSRUCodeForAccount('4010')).toBe('7511')
expect(findSRUCodeForAccount('4499')).toBe('7511')
})
it('4600-4699 -> 7512 (Handelsvaror)', () => {
expect(findSRUCodeForAccount('4600')).toBe('7512')
expect(findSRUCodeForAccount('4699')).toBe('7512')
})
it('5000-6999 ALL -> 7513 (Övriga externa kostnader)', () => {
expect(findSRUCodeForAccount('5000')).toBe('7513')
expect(findSRUCodeForAccount('5460')).toBe('7513')
expect(findSRUCodeForAccount('6200')).toBe('7513')
expect(findSRUCodeForAccount('6999')).toBe('7513')
})
it('7000-7699 -> 7514 (Personalkostnader)', () => {
expect(findSRUCodeForAccount('7000')).toBe('7514')
expect(findSRUCodeForAccount('7210')).toBe('7514')
expect(findSRUCodeForAccount('7699')).toBe('7514')
})
it('7800-7899 -> 7515 (Avskrivningar)', () => {
expect(findSRUCodeForAccount('7800')).toBe('7515')
expect(findSRUCodeForAccount('7820')).toBe('7515')
expect(findSRUCodeForAccount('7899')).toBe('7515')
})
it('7700-7799 -> 7516 (Nedskrivningar OT)', () => {
expect(findSRUCodeForAccount('7700')).toBe('7516')
expect(findSRUCodeForAccount('7799')).toBe('7516')
})
it('7900-7999 -> 7517 (Övriga rörelsekostnader)', () => {
expect(findSRUCodeForAccount('7900')).toBe('7517')
expect(findSRUCodeForAccount('7999')).toBe('7517')
})
it('8300-8399 -> 7417 (Ränteintäkter)', () => {
expect(findSRUCodeForAccount('8300')).toBe('7417')
expect(findSRUCodeForAccount('8310')).toBe('7417')
})
it('8400-8499 -> 7522 (Räntekostnader)', () => {
expect(findSRUCodeForAccount('8400')).toBe('7522')
expect(findSRUCodeForAccount('8499')).toBe('7522')
})
it('8900-8989 -> 7528 (Skatt)', () => {
expect(findSRUCodeForAccount('8900')).toBe('7528')
expect(findSRUCodeForAccount('8910')).toBe('7528')
})
describe('bokslutsdispositioner (BAS 2020-aligned)', () => {
// These mappings were corrected when the Phase 2 bokslut calculators
// landed: the previous ranges (8810/8830/8840) targeted accounts that
// BAS doesn't seed. Locking the corrected mapping prevents regression.
it('8811 -> 7525 (Avsättning till periodiseringsfond)', () => {
expect(findSRUCodeForAccount('8811')).toBe('7525')
})
it('8819 -> 7420 (Återföring av periodiseringsfond)', () => {
expect(findSRUCodeForAccount('8819')).toBe('7420')
})
it('8820 -> 7419 (Mottagna koncernbidrag)', () => {
expect(findSRUCodeForAccount('8820')).toBe('7419')
})
it('8830 -> 7524 (Lämnade koncernbidrag)', () => {
expect(findSRUCodeForAccount('8830')).toBe('7524')
})
it('8850-8859 -> 7421 (Förändring av överavskrivningar)', () => {
expect(findSRUCodeForAccount('8850')).toBe('7421')
expect(findSRUCodeForAccount('8853')).toBe('7421') // M&I sub-cat
expect(findSRUCodeForAccount('8859')).toBe('7421')
})
it('8840 + 8860-8899 -> 7422 (Övriga bokslutsdispositioner)', () => {
expect(findSRUCodeForAccount('8840')).toBe('7422')
expect(findSRUCodeForAccount('8860')).toBe('7422')
expect(findSRUCodeForAccount('8899')).toBe('7422')
})
})
})
describe('no overlap between mappings', () => {
it('representative boundary accounts match exactly one mapping', () => {
const testAccounts = [
'1079', '1080', // 7201/7202 boundary
'1089', '1090', // 7202/7201 boundary
'1099', '1100', // 7201/7214 boundary
'1119', '1120', // 7214/7216 boundary
'1129', '1130', // 7216/7214 boundary
'1199', '1200', // 7214/7215 boundary
'1299', '1311', // 7215/7230 boundary
'1519', '1520', // 7251/7261 boundary
'1559', '1560', // 7261/7252 boundary
'1930', '1999', // 7281 bank accounts
'2089', '2090', // 7301/7302 boundary
'2099', '2110', // 7302/7321 boundary
'2439', '2440', // 7361/7365 boundary
'2449', '2450', // 7365/7363 boundary
'2499', '2500', // 7369/7368 boundary
'2599', '2600', // 7368/7369 boundary
'2899', '2900', // 7369/7370 boundary
'3799', '3800', // 7410/7412 boundary
'3899', '3900', // 7412/7413 boundary
'3999', '4000', // 7413/7511 boundary
'4499', '4600', // 7511/7512 boundary (4500-4599 unmapped)
'5000', '6999', // 7513 (övriga externa)
'7699', '7700', // 7514/7516 boundary
'7799', '7800', // 7516/7515 boundary
'7899', '7900', // 7515/7517 boundary
'8399', '8400', // 7417/7522 boundary
]
for (const account of testAccounts) {
let matchCount = 0
for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
if (isAccountInMapping(account, mapping)) {
matchCount++
}
}
expect(matchCount, `Account ${account} should match exactly one mapping, got ${matchCount}`).toBe(1)
}
})
})
describe('section assignments', () => {
it('all asset mappings have section "assets"', () => {
const assetMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'assets')
expect(assetMappings.length).toBe(26)
for (const m of assetMappings) {
expect(m.normalBalance).toBe('debit')
}
})
it('all equity/liability mappings have section "equity_liabilities"', () => {
const eqMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'equity_liabilities')
expect(eqMappings.length).toBe(24)
for (const m of eqMappings) {
expect(m.normalBalance).toBe('credit')
}
})
it('income statement mappings have correct normal balance types', () => {
const isMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'income_statement')
expect(isMappings.length).toBeGreaterThanOrEqual(20)
// Revenue accounts are credit-normal
const revenue = isMappings.find(m => m.sruCode === '7410')
expect(revenue?.normalBalance).toBe('credit')
// Cost accounts are debit-normal
const costs = isMappings.find(m => m.sruCode === '7513')
expect(costs?.normalBalance).toBe('debit')
// Net items
const net = isMappings.find(m => m.sruCode === '7414')
expect(net?.normalBalance).toBe('net')
})
})
})
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')
})
})