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:
co-authored by
Claude Fable 5
parent
24911abde0
commit
bb1eddcccf
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user