Files
accounted/lib/bokslut/__tests__/latent-tax-calculator.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

97 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import {
computeLatentTax,
proposeLatentTaxChange,
LATENT_TAX_DEFAULT_RATE,
LATENT_TAX_LIABILITY_ACCOUNT,
LATENT_TAX_EXPENSE_ACCOUNT,
} from '../tax-provision/latent-tax-calculator'
describe('computeLatentTax', () => {
it('splits 100 000 reserves into 79 400 equity + 20 600 liability at default 20,6 %', () => {
const split = computeLatentTax({ untaxedReserves: 100_000 })
expect(split.liabilityPortion).toBe(20_600)
expect(split.equityPortion).toBe(79_400)
// Invariant: the two portions reconcile to the input.
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(100_000, 2)
})
it('returns zero portions for zero reserves', () => {
const split = computeLatentTax({ untaxedReserves: 0 })
expect(split.equityPortion).toBe(0)
expect(split.liabilityPortion).toBe(0)
})
it('preserves the sign for negative reserves (over-reversal edge case)', () => {
// Unusual but the math should stay symmetric: e.g. when the
// dispositions builder posts more återföring than the existing reserves.
const split = computeLatentTax({ untaxedReserves: -50_000 })
expect(split.liabilityPortion).toBe(-10_300)
expect(split.equityPortion).toBe(-39_700)
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(-50_000, 2)
})
it('accepts a custom tax rate (future flex for rate changes)', () => {
// If bolagsskatt drops to e.g. 18 %, K3 split would follow.
const split = computeLatentTax({ untaxedReserves: 100_000, taxRate: 0.18 })
expect(split.liabilityPortion).toBe(18_000)
expect(split.equityPortion).toBe(82_000)
})
it('rounds to öre on non-integer reserves', () => {
// 12 345.67 × 0.206 = 2 543.20802 → öre rounding → 2 543.21
// equity = 12 345.67 − 2 543.21 = 9 802.46
const split = computeLatentTax({ untaxedReserves: 12_345.67 })
expect(split.liabilityPortion).toBe(2_543.21)
expect(split.equityPortion).toBe(9_802.46)
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(12_345.67, 2)
})
it('exports the canonical 20.6 % rate constant', () => {
expect(LATENT_TAX_DEFAULT_RATE).toBe(0.206)
})
})
describe('proposeLatentTaxChange', () => {
it('returns null when current already equals target (no change)', () => {
expect(proposeLatentTaxChange(20_600, 20_600)).toBeNull()
})
it('returns null when delta is below 1 öre tolerance', () => {
// Floating-point dust below 1 öre should not produce a verifikat.
expect(proposeLatentTaxChange(20_600, 20_600.001)).toBeNull()
expect(proposeLatentTaxChange(20_600.0049, 20_600)).toBeNull()
})
it('books an avsättning when liability grows: debit 8940 / credit 2240', () => {
const lines = proposeLatentTaxChange(0, 20_600)
expect(lines).not.toBeNull()
expect(lines).toHaveLength(2)
const debit = lines!.find((l) => l.account_number === LATENT_TAX_EXPENSE_ACCOUNT)!
const credit = lines!.find((l) => l.account_number === LATENT_TAX_LIABILITY_ACCOUNT)!
expect(debit.debit_amount).toBe(20_600)
expect(debit.credit_amount).toBe(0)
expect(credit.debit_amount).toBe(0)
expect(credit.credit_amount).toBe(20_600)
})
it('books a återföring when liability shrinks: debit 2240 / credit 8940', () => {
const lines = proposeLatentTaxChange(20_600, 15_000)
expect(lines).not.toBeNull()
expect(lines).toHaveLength(2)
const debit = lines!.find((l) => l.account_number === LATENT_TAX_LIABILITY_ACCOUNT)!
const credit = lines!.find((l) => l.account_number === LATENT_TAX_EXPENSE_ACCOUNT)!
expect(debit.debit_amount).toBe(5_600)
expect(credit.credit_amount).toBe(5_600)
})
it('produces a balanced verifikat (sum debit = sum credit)', () => {
const lines = proposeLatentTaxChange(10_000, 18_000)
expect(lines).not.toBeNull()
const totalDebit = lines!.reduce((s, l) => s + l.debit_amount, 0)
const totalCredit = lines!.reduce((s, l) => s + l.credit_amount, 0)
expect(totalDebit).toBe(totalCredit)
expect(totalDebit).toBe(8_000)
})
})