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>
195 lines
6.4 KiB
TypeScript
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')
|
|
})
|
|
})
|