Files
accounted/lib/bokslut/__tests__/tax-depreciation.test.ts
T
MattssonandClaude Fable 5 5ca64bde30 feat(bokslut): IL 18 kap pooled tax depreciation with method election (#1393)
* feat(bokslut): IL 18 kap pooled tax depreciation with method election

Rakenskapsenlig (huvudregel 30 / kompletteringsregel 20) and restvarde 25
as a company-level annual pool separate from per-asset book depreciation.
Method election persisted with immutable snapshots and book-conformity
confirmation for rakenskapsenlig.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(db): move tax depreciation migrations to coordinated versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): keep tax depreciation view loadable when a saved election goes stale

A predecessor's changed closing value can push a saved elected deduction
above the new statutory maximum; the view now falls back to the statutory
recomputation so the snapshot is flagged stale instead of crashing.
Ratchet naive-ore-round baseline down by 3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): resolve tax-depreciation period selects statically

The no-phantom-columns guard counts every select it cannot resolve
toward a hard ceiling, and the PERIOD_COLUMNS join pushed the repo
4 over (364 > 360). Inline the literal column list at the four call
sites so the guard verifies these columns instead of skipping them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bokslut): address review findings on tax depreciation election

- DepreciationPanel: gate the saving flag on a dedicated save sequence
  so a successful save (which refreshes the view and bumps the request
  version) no longer leaves the card permanently busy
- computeTaxDepreciation: refuse kompletteringsregel_20 with a positive
  basis and no acquisition cohorts instead of degenerating to a full
  write-off the cohort evidence does not support (IL 18 kap. 17 §)
- migration 227000: judge the asset-method guards on NEW.disposed_at so
  reversing a disposal cannot reactivate a grandfathered non-linear row
- migration 227200: require snapshot column completeness in the CHECK;
  SQL NULL semantics let partially populated snapshots pass the pure
  arithmetic comparisons
- depreciation route: use the string issue code 'custom' like the rest
  of the codebase instead of the Zod 3 compat enum

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 11:43:58 +02:00

173 lines
5.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
computeTaxDepreciation,
fiscalPeriodMonths,
} from '../assets/tax-depreciation'
describe('computeTaxDepreciation', () => {
it('calculates the pooled 30 percent main rule after additions and disposals', () => {
const result = computeTaxDepreciation({
method: 'rakenskapsenlig',
selectedRule: 'huvudregel_30',
openingTaxValue: 100_000,
additions: 50_000,
disposals: 20_000,
periodMonths: 12,
cohorts: [],
})
expect(result.basis).toBe(130_000)
expect(result.maximumDeduction).toBe(39_000)
expect(result.deduction).toBe(39_000)
expect(result.closingTaxValue).toBe(91_000)
})
it('reduces only by net disposal proceeds and floors a negative basis at zero', () => {
const result = computeTaxDepreciation({
method: 'restvarde',
openingTaxValue: 10_000,
additions: 0,
disposals: 12_500,
periodMonths: 12,
cohorts: [],
})
expect(result.basis).toBe(0)
expect(result.excessDisposals).toBe(2_500)
expect(result.deduction).toBe(0)
})
it('uses 80, 60, 40, 20 and 0 percent closing cohorts for the 20 percent rule', () => {
const result = computeTaxDepreciation({
method: 'rakenskapsenlig',
selectedRule: 'kompletteringsregel_20',
openingTaxValue: 400_000,
additions: 100_000,
disposals: 0,
periodMonths: 12,
cohorts: [12, 24, 36, 48, 60].map((elapsedMonths, index) => ({
label: String(index),
acquisitionCost: 100_000,
elapsedMonths,
})),
})
expect(result.cohorts.map((cohort) => cohort.closingValue)).toEqual([
80_000,
60_000,
40_000,
20_000,
0,
])
expect(result.closingTaxValue).toBe(200_000)
expect(result.deduction).toBe(300_000)
})
it('lets the annual räkenskapsenlig election switch between 30 and 20 alternatives', () => {
const shared = {
method: 'rakenskapsenlig' as const,
openingTaxValue: 70_000,
additions: 100_000,
disposals: 0,
periodMonths: 12,
cohorts: [{ label: '2025', acquisitionCost: 100_000, elapsedMonths: 12 }],
}
const main = computeTaxDepreciation({ ...shared, selectedRule: 'huvudregel_30' })
const complement = computeTaxDepreciation({
...shared,
selectedRule: 'kompletteringsregel_20',
})
expect(main.closingTaxValue).toBe(119_000)
expect(complement.closingTaxValue).toBe(80_000)
expect(main.alternatives).toEqual(complement.alternatives)
})
it('stores an elected deduction below the statutory maximum without changing the maximum', () => {
const result = computeTaxDepreciation({
method: 'rakenskapsenlig',
selectedRule: 'huvudregel_30',
openingTaxValue: 100_000,
additions: 0,
disposals: 0,
periodMonths: 12,
cohorts: [],
electedDeduction: 20_000,
})
expect(result.maximumDeduction).toBe(30_000)
expect(result.deduction).toBe(20_000)
expect(result.closingTaxValue).toBe(80_000)
})
it('rejects an elected deduction above the statutory maximum', () => {
expect(() => computeTaxDepreciation({
method: 'restvarde',
openingTaxValue: 100_000,
additions: 0,
disposals: 0,
periodMonths: 12,
cohorts: [],
electedDeduction: 25_001,
})).toThrow(/must not exceed the statutory maximum/)
})
it('adjusts 30 and 25 percent proportionally for short and long fiscal periods', () => {
const short = computeTaxDepreciation({
method: 'rakenskapsenlig',
selectedRule: 'huvudregel_30',
openingTaxValue: 100_000,
additions: 0,
disposals: 0,
periodMonths: 6,
cohorts: [],
})
const long = computeTaxDepreciation({
method: 'restvarde',
openingTaxValue: 100_000,
additions: 0,
disposals: 0,
periodMonths: 18,
cohorts: [],
})
expect(short.deduction).toBe(15_000)
expect(long.deduction).toBe(37_500)
})
it('adjusts the 20 percent cohort rate for a short fiscal period', () => {
const result = computeTaxDepreciation({
method: 'rakenskapsenlig',
selectedRule: 'kompletteringsregel_20',
openingTaxValue: 0,
additions: 100_000,
disposals: 0,
periodMonths: 6,
cohorts: [{ label: 'short', acquisitionCost: 100_000, elapsedMonths: 6 }],
})
expect(result.closingTaxValue).toBe(90_000)
expect(result.deduction).toBe(10_000)
})
it('rejects a 20 percent rule selection for rest value depreciation', () => {
expect(() => computeTaxDepreciation({
method: 'restvarde',
selectedRule: 'kompletteringsregel_20',
openingTaxValue: 100_000,
additions: 0,
disposals: 0,
periodMonths: 12,
cohorts: [],
})).toThrow(/only valid for rakenskapsenlig/)
})
})
describe('fiscalPeriodMonths', () => {
it('counts calendar months inclusively for normal, short and long years', () => {
expect(fiscalPeriodMonths('2025-01-01', '2025-12-31')).toBe(12)
expect(fiscalPeriodMonths('2025-07-01', '2025-12-31')).toBe(6)
expect(fiscalPeriodMonths('2024-07-01', '2025-12-31')).toBe(18)
})
})