Files
accounted/extensions/general/skatteverket/__tests__/mappers.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

195 lines
6.4 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { rutorToMomsuppgift, formatRedovisare, formatRedovisningsperiod } from '../lib/mappers'
import type { VatDeclarationRutor } from '@/types'
const emptyRutor: VatDeclarationRutor = {
ruta05: 0, ruta06: 0, ruta07: 0, ruta08: 0,
ruta10: 0, ruta11: 0, ruta12: 0,
ruta20: 0, ruta21: 0, ruta22: 0, ruta23: 0, ruta24: 0,
ruta30: 0, ruta31: 0, ruta32: 0,
ruta35: 0, ruta36: 0, ruta37: 0, ruta38: 0,
ruta39: 0, ruta40: 0, ruta41: 0, ruta42: 0,
ruta48: 0, ruta49: 0,
ruta50: 0, ruta60: 0, ruta61: 0, ruta62: 0,
}
describe('rutorToMomsuppgift', () => {
it('converts typical EF VAT declaration', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 10000,
ruta10: 2500,
ruta48: 350,
ruta49: 2150,
}
const result = rutorToMomsuppgift(rutor)
expect(result.momspliktigForsaljning).toBe(10000)
expect(result.momsForsaljningUtgaendeHog).toBe(2500)
expect(result.ingaendeMomsAvdrag).toBe(350)
expect(result.summaMoms).toBe(2150)
})
it('omits zero-value fields except summaMoms', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta05: 5000,
ruta10: 1250,
ruta49: 1250,
}
const result = rutorToMomsuppgift(rutor)
expect(result.momspliktigForsaljning).toBe(5000)
expect(result.momsForsaljningUtgaendeHog).toBe(1250)
expect(result.summaMoms).toBe(1250)
// Zero fields should not be present
expect(result.momspliktigaUttag).toBeUndefined()
expect(result.momsForsaljningUtgaendeMedel).toBeUndefined()
expect(result.momsForsaljningUtgaendeLag).toBeUndefined()
expect(result.ingaendeMomsAvdrag).toBeUndefined()
})
it('always includes summaMoms even when zero', () => {
const result = rutorToMomsuppgift(emptyRutor)
expect(result.summaMoms).toBe(0)
})
it('maps reverse charge fields correctly', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta21: 5000,
ruta30: 1250,
ruta48: 1250,
ruta49: 0,
}
const result = rutorToMomsuppgift(rutor)
expect(result.inkopTjansterEU).toBe(5000)
expect(result.momsInkopUtgaendeHog).toBe(1250)
expect(result.ingaendeMomsAvdrag).toBe(1250)
expect(result.summaMoms).toBe(0)
})
it('maps EU/export and import fields', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta35: 8000,
ruta36: 12000,
ruta39: 3000,
ruta40: 5000,
ruta50: 2000,
ruta60: 500,
ruta49: 500,
}
const result = rutorToMomsuppgift(rutor)
expect(result.forsaljningVarorEU).toBe(8000)
expect(result.forsaljningVarorUtanforEU).toBe(12000)
expect(result.forsaljningTjansterEU).toBe(3000)
expect(result.ovrigForsaljningTjansterUtanforSE).toBe(5000)
expect(result.import).toBe(2000)
expect(result.momsImportUtgaendeHog).toBe(500)
})
// FK009 regression: SKV recomputes summaMoms from rounded rutor and
// compares against ours. If we round ruta49 from unrounded inputs while
// rounding each ruta independently we drift by ±1 SEK per fractional ruta.
it('summaMoms equals Σ(rounded output VAT rutor) - rounded ruta48', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
// Fractional öres on every VAT-amount ruta so banker's rounding can
// disagree between the orundad ruta49 and the rundade individual fält.
ruta10: 100.49,
ruta11: 50.51,
ruta12: 25.49,
ruta30: 10.51,
ruta31: 5.49,
ruta32: 2.51,
ruta60: 7.49,
ruta61: 3.51,
ruta62: 1.49,
ruta48: 80.51,
ruta49: 100.49 + 50.51 + 25.49 + 10.51 + 5.49 + 2.51 + 7.49 + 3.51 + 1.49 - 80.51,
}
const result = rutorToMomsuppgift(rutor)
const expectedSumma =
(result.momsForsaljningUtgaendeHog ?? 0) +
(result.momsForsaljningUtgaendeMedel ?? 0) +
(result.momsForsaljningUtgaendeLag ?? 0) +
(result.momsInkopUtgaendeHog ?? 0) +
(result.momsInkopUtgaendeMedel ?? 0) +
(result.momsInkopUtgaendeLag ?? 0) +
(result.momsImportUtgaendeHog ?? 0) +
(result.momsImportUtgaendeMedel ?? 0) +
(result.momsImportUtgaendeLag ?? 0) -
(result.ingaendeMomsAvdrag ?? 0)
expect(result.summaMoms).toBe(expectedSumma)
// Sanity-check: result is an integer (SKV requires whole kronor)
expect(Number.isInteger(result.summaMoms)).toBe(true)
})
it('summaMoms is negative when input VAT exceeds output VAT', () => {
const rutor: VatDeclarationRutor = {
...emptyRutor,
ruta48: 5000,
ruta49: -5000,
}
const result = rutorToMomsuppgift(rutor)
expect(result.summaMoms).toBe(-5000)
})
})
describe('formatRedovisare', () => {
it('formats aktiebolag org number with 16 prefix', () => {
expect(formatRedovisare('5020000013', 'aktiebolag')).toBe('165020000013')
})
it('formats aktiebolag org number with hyphen', () => {
expect(formatRedovisare('502000-0013', 'aktiebolag')).toBe('165020000013')
})
it('formats enskild firma personnummer with 19 prefix for older', () => {
// Person born in 1985: 85 > 26 (current year), so prefix 19
expect(formatRedovisare('8501011234', 'enskild_firma')).toBe('198501011234')
})
it('formats enskild firma personnummer with 20 prefix for younger', () => {
// Person born in 2005: 05 < 26 (current year), so prefix 20
expect(formatRedovisare('0501011234', 'enskild_firma')).toBe('200501011234')
})
it('passes through 12-digit numbers unchanged', () => {
expect(formatRedovisare('165020000013', 'aktiebolag')).toBe('165020000013')
})
it('throws for invalid length', () => {
expect(() => formatRedovisare('12345', 'aktiebolag')).toThrow('Ogiltigt organisationsnummer')
})
})
describe('formatRedovisningsperiod', () => {
it('formats monthly period as YYYYMM', () => {
expect(formatRedovisningsperiod('monthly', 2025, 1)).toBe('202501')
expect(formatRedovisningsperiod('monthly', 2025, 12)).toBe('202512')
})
it('formats quarterly period as last month of quarter', () => {
expect(formatRedovisningsperiod('quarterly', 2025, 1)).toBe('202503')
expect(formatRedovisningsperiod('quarterly', 2025, 2)).toBe('202506')
expect(formatRedovisningsperiod('quarterly', 2025, 3)).toBe('202509')
expect(formatRedovisningsperiod('quarterly', 2025, 4)).toBe('202512')
})
it('formats yearly period as December', () => {
expect(formatRedovisningsperiod('yearly', 2025, 1)).toBe('202512')
})
})