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>
164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
||
import { calculateTraktamente, calculateMileageAllowance } from '../traktamente'
|
||
import type { PayrollConfig } from '../payroll-config'
|
||
|
||
const config: PayrollConfig = {
|
||
configYear: 2026,
|
||
avgifterTotal: 0.3142,
|
||
avgifterAlderspension: 0.1021,
|
||
avgifterSjukforsakring: 0.0355,
|
||
avgifterForaldraforsakring: 0.0200,
|
||
avgifterEfterlevandepension: 0.0030,
|
||
avgifterArbetsmarknad: 0.0264,
|
||
avgifterArbetsskada: 0.0010,
|
||
avgifterAllmanLoneavgift: 0.1262,
|
||
avgifterReduced65plus: 0.1021,
|
||
avgifterYouthRate: 0.2081,
|
||
avgifterYouthSalaryCap: 25000,
|
||
avgifterVaxaStodRate: 0.1021,
|
||
avgifterVaxaStodCap: 35000,
|
||
avgifterMinimumAnnual: 1000,
|
||
egenavgifterTotal: 0.2897,
|
||
slpRate: 0.2426,
|
||
prisbasbelopp: 59200,
|
||
inkomstbasbelopp: 83400,
|
||
maxPgi: 625500,
|
||
sgiCeiling: 592000,
|
||
statligSkattBrytpunkt: 660400,
|
||
traktamenteHeldag: 300,
|
||
traktamenteHalvdag: 150,
|
||
traktamenteNatt: 150,
|
||
milersattningEgenBil: 25,
|
||
milersattningFormansbilFossil: 12,
|
||
milersattningFormansbilEl: 9.50,
|
||
kostformanHeldag: 310,
|
||
kostformanLunch: 124,
|
||
kostformanFrukost: 62,
|
||
friskvardCap: 5000,
|
||
bilformanSlr: 0.0255,
|
||
sjuklonRate: 0.80,
|
||
karensavdragFactor: 0.20,
|
||
maxKarensavdragPerYear: 10,
|
||
reducedAvgiftAge: 67,
|
||
}
|
||
|
||
describe('calculateTraktamente', () => {
|
||
it('calculates full-day traktamente at 300 SEK', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'full_day',
|
||
days: 3,
|
||
mealsProvided: 'none',
|
||
consecutiveMonths: 0,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(900) // 300 × 3
|
||
expect(result.taxable).toBe(0)
|
||
})
|
||
|
||
it('calculates half-day at 150 SEK', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'half_day',
|
||
days: 2,
|
||
mealsProvided: 'none',
|
||
consecutiveMonths: 0,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(300) // 150 × 2
|
||
})
|
||
|
||
it('applies tremånadersregeln: 70% after 3 months', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'full_day',
|
||
days: 1,
|
||
mealsProvided: 'none',
|
||
consecutiveMonths: 4,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(Math.round(300 * 0.70 * 100) / 100) // 210
|
||
// If employer pays full rate (300), excess is taxable
|
||
expect(result.taxable).toBe(Math.round((300 - 210) * 100) / 100) // 90
|
||
})
|
||
|
||
it('applies tremånadersregeln: 50% after 2 years', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'full_day',
|
||
days: 1,
|
||
mealsProvided: 'none',
|
||
consecutiveMonths: 25,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(150) // 300 × 50%
|
||
expect(result.taxable).toBe(150) // 300 - 150
|
||
})
|
||
|
||
it('reduces for meals provided: lunch reduces by 35%', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'full_day',
|
||
days: 1,
|
||
mealsProvided: 'lunch',
|
||
consecutiveMonths: 0,
|
||
config,
|
||
})
|
||
const mealReduction = Math.round(300 * 0.35 * 100) / 100
|
||
expect(result.taxFree).toBe(Math.round((300 - mealReduction) * 100) / 100)
|
||
})
|
||
|
||
it('reduces for all meals: 85%', () => {
|
||
const result = calculateTraktamente({
|
||
tripType: 'full_day',
|
||
days: 1,
|
||
mealsProvided: 'all',
|
||
consecutiveMonths: 0,
|
||
config,
|
||
})
|
||
const mealReduction = Math.round(300 * 0.85 * 100) / 100
|
||
expect(result.taxFree).toBe(Math.round((300 - mealReduction) * 100) / 100)
|
||
})
|
||
})
|
||
|
||
describe('calculateMileageAllowance', () => {
|
||
it('calculates own car at 25 SEK/mil', () => {
|
||
const result = calculateMileageAllowance({
|
||
mil: 10,
|
||
vehicleType: 'own_car',
|
||
paidPerMil: 25,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(250) // 25 × 10
|
||
expect(result.taxable).toBe(0)
|
||
})
|
||
|
||
it('calculates company car fossil at 12 SEK/mil', () => {
|
||
const result = calculateMileageAllowance({
|
||
mil: 10,
|
||
vehicleType: 'company_car_fossil',
|
||
paidPerMil: 12,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(120)
|
||
expect(result.taxable).toBe(0)
|
||
})
|
||
|
||
it('calculates company car electric at 9.50 SEK/mil', () => {
|
||
const result = calculateMileageAllowance({
|
||
mil: 10,
|
||
vehicleType: 'company_car_electric',
|
||
paidPerMil: 9.50,
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(95)
|
||
expect(result.taxable).toBe(0)
|
||
})
|
||
|
||
it('marks excess as taxable when paid above tax-free rate', () => {
|
||
const result = calculateMileageAllowance({
|
||
mil: 10,
|
||
vehicleType: 'own_car',
|
||
paidPerMil: 30, // 5 above tax-free
|
||
config,
|
||
})
|
||
expect(result.taxFree).toBe(250) // 25 × 10
|
||
expect(result.taxable).toBe(50) // (30-25) × 10
|
||
})
|
||
})
|