fix(salary): exempt F-skatt compensation from arbetsgivaravgifter (#1372)

* fix(salary): exempt F-skatt compensation from arbetsgivaravgifter

calculateAvgifterRate() never checked fSkattStatus, so F-skatt earners
were charged the standard 31.42% employer contributions even though
they pay their own egenavgifter. This overstated employer cost by ~31%
and booked incorrect 7510/2731 entries.

Add an early return in calculateAvgifterRate for f_skatt (rate 0,
category exempt) before any age-based rules, and zero the avgifter
basis in calculateSalary so salary reports and AGI totals do not carry
a false contribution basis. The FK011/FK131 AGI rendering defect stays
scoped to issue #315.

Fixes #314

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

* test(salary): assert full exempt return contract for F-skatt

CodeRabbit review: calculateSalary does not read amount/basis from
AvgifterCalculation, so the direct-return test must pin both to zero
to catch a regression in the exempt early return.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-03 17:56:09 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 24911abde0
commit bb1eddcccf
3 changed files with 39 additions and 2 deletions
+2
View File
@@ -753,3 +753,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-03] CI gained a pg-upgrade job: apply the merge-base schema, seed real rows, apply ONLY the PR migrations, assert the data survived. Rationale: pg-real applies all 548 migrations to an EMPTY database, so a NOT NULL / CHECK / unique index / backfill passes against zero rows and can still break prod. Proven locally against supabase/postgres:15.8.1.060 with three bad migrations: a CHECK violating an ore-level row and a NOT NULL on a populated column both exit 0 on empty and exit 3 on seeded. Base migrations are read from the merge-base git tree, not the working tree, so a PR that edits a shipped migration still surfaces here.
[2026-08-03] Issue #323 automatic excess depreciation is limited to reconciled IL 18 machinery and equipment with linear book depreciation and posts 8853/2153: buildings, intangible assets, and the 25 percent rest-value method follow separate rules, so calculation fails closed on an incomplete register or unposted planned depreciation.
[2026-08-03] Issue #314 zeroes the F-skatt avgifter basis at the calculation boundary as well as the rate: a rate-only exemption would stop the 7510/2731 charge but leave a false contribution basis in salary reports and AGI totals; the separate FK011/FK131 XML rendering defect remains scoped to issue #315.
@@ -152,7 +152,7 @@ describe('calculateSalary', () => {
expect(result.taxWithheld).toBe(12000) // 30% of 40000
})
it('applies f-skatt with 0% withholding', () => {
it('exempts F-skatt compensation from withholding and employer contributions', () => {
const result = calculateSalary(
makeBasicInput({ fSkattStatus: 'f_skatt' }),
config2026,
@@ -161,6 +161,12 @@ describe('calculateSalary', () => {
expect(result.taxWithheld).toBe(0)
expect(result.netSalary).toBe(40000)
expect(result.avgifterRate).toBe(0)
expect(result.avgifterAmount).toBe(0)
expect(result.avgifterBasis).toBe(0)
expect(result.avgifterCategory).toBe('exempt')
expect(result.vacationAccrualAvgifter).toBe(0)
expect(result.totalEmployerCost).toBe(result.grossSalary + result.vacationAccrual)
})
it('applies unverified flat 30%', () => {
@@ -1050,6 +1056,25 @@ describe('calculateSjuklon', () => {
})
describe('calculateAvgifterRate', () => {
it('returns the exempt rate for F-skatt before age-based rules', () => {
const result = calculateAvgifterRate(
makeBasicInput({ fSkattStatus: 'f_skatt', personnummer: 'mock_senior_person' }),
config2026,
2026
)
expect(result.rate).toBe(0)
expect(result.amount).toBe(0)
expect(result.basis).toBe(0)
expect(result.category).toBe('exempt')
expect(result.steps).toEqual([
expect.objectContaining({
label: 'Avgiftskategori',
output: null,
}),
])
})
it('returns standard rate for normal employee', () => {
const result = calculateAvgifterRate(
makeBasicInput(),
+11 -1
View File
@@ -475,7 +475,7 @@ export function calculateSalary(
// ─── Step 8: Employer contributions (avgifter) ───
const avgifterCalc = calculateAvgifterRate(input, config, paymentYear)
const avgifterBasis = r(grossSalary + totalBenefits)
const avgifterBasis = input.fSkattStatus === 'f_skatt' ? 0 : r(grossSalary + totalBenefits)
// Handle salary caps for youth and växa-stöd:
// Reduced rate applies only up to the cap, standard rate on the rest
@@ -632,6 +632,16 @@ export function calculateAvgifterRate(
): AvgifterCalculation {
const steps: CalculationStep[] = []
if (input.fSkattStatus === 'f_skatt') {
steps.push({
label: 'Avgiftskategori',
formula: 'F-skatt: inga arbetsgivaravgifter',
input: {},
output: null,
})
return { rate: 0, amount: 0, basis: 0, category: 'exempt', steps }
}
// Decrypt personnummer to calculate age
let pnr: string
try {