fix(salary): report F-skatt compensation on FK131 only, not FK011 too (#1402)

* fix(salary): report F-skatt compensation on FK131 only, not FK011 too

An F-skatt payee's cash compensation was passed through as grossSalary
unconditionally while also being routed to fSkattPayment, so the same
payment was double-reported in the AGI individuppgift as both FK011
(KontantErsattningUlagAG, underlag for arbetsgivaravgifter) and FK131
(KontantErsattningEjUlagSA). Per Skatteverket's AGI spec these fields
are mutually exclusive for the same payment stream: F-skatt payments
form no underlag for arbetsgivaravgifter.

Fix at the data layer: generateAgiDeclaration now zeroes grossSalary
for f_skatt payees so FK011 is never emitted for that payment, while
fSkattPayment keeps carrying it to FK131. A generator-side guard was
deliberately not used because an IU can legitimately carry both fields
for genuinely mixed payments.

The empty-IU filter already keeps f_skatt rows via fSkattPayment, so no
individuppgift is dropped; FK487/avgifter totals were already correct
(calculation engine sets avgifter_basis 0 for f_skatt) and are covered
by a regression test. Both the dashboard route and the v1 public route
call this helper, so both are fixed.

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

* fix(salary): exclude f-skatt rows from avgifter override aggregation (#315)

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

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-04 19:33:36 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent f80e933f8d
commit 97bbba323f
3 changed files with 299 additions and 9 deletions
+25 -9
View File
@@ -128,6 +128,16 @@ function sumLineItemAmounts(
.reduce((sum, li) => sum + ((li.amount as number) || 0), 0)
}
// Invariant: F-skatt compensation never contributes to the avgifter
// aggregates (per-IU basis, FK061-series categories, FK487, HU totals),
// overrides included. The calculation engine already stores
// avgifter_basis/avgifter_amount = 0 for these rows; a manual advanced-mode
// override must not resurrect them, or the filing would claim social charges
// on pay whose IU simultaneously asserts FK131 (not subject to them).
function isFSkattRow(sre: SalaryRunEmployeeRow): boolean {
return sre.employee?.f_skatt_status === 'f_skatt'
}
export async function generateAgiDeclaration(
args: GenerateAgiDeclarationArgs,
): Promise<GenerateAgiDeclarationResult> {
@@ -330,21 +340,25 @@ export async function generateAgiDeclaration(
)
}
const isFSkatt = emp?.f_skatt_status === 'f_skatt'
const isFSkatt = isFSkattRow(sre)
// Honor advanced-mode per-employee overrides set during review.
// F-skatt rows ignore avgifter overrides (see isFSkattRow invariant).
const effectiveTax = sre.tax_withheld_override ?? sre.tax_withheld
const effectiveAvgifterBasis = sre.avgifter_basis_override ?? sre.avgifter_basis
const effectiveAvgifterBasis = isFSkatt
? 0
: sre.avgifter_basis_override ?? sre.avgifter_basis
return {
personnummer: emp?.personnummer ?? '',
specificationNumber: emp?.specification_number ?? 0,
removed: Boolean(sre.removed_from_agi),
grossSalary: sre.gross_salary,
grossSalary: isFSkatt ? 0 : sre.gross_salary,
taxWithheld: effectiveTax,
avgifterBasis: effectiveAvgifterBasis,
fSkattPayment: isFSkatt ? sre.gross_salary : undefined,
// F-skatt payees: cash goes to FK131 and benefits to the ej-UlagSA
// variants (FK132/FK133/FK134/FK137/FK138/FK139). Regular employees
// get FK011 + FK012/FK013/FK015/FK018/FK041/FK043.
// F-skatt payees: cash goes to FK131 ONLY (grossSalary is zeroed so
// FK011 is never emitted for the same payment) and benefits to the
// ej-UlagSA variants (FK132/FK133/FK134/FK137/FK138/FK139). Regular
// employees get FK011 + FK012/FK013/FK015/FK018/FK041/FK043.
benefitsExcludedFromSAUnderlag: isFSkatt ? true : undefined,
benefitCar: benefitCar > 0 ? benefitCar : undefined,
benefitFuel: benefitFuel > 0 ? benefitFuel : undefined,
@@ -405,8 +419,9 @@ export async function generateAgiDeclaration(
const cat = (avgifterByCategory as Record<string, { basis: number; amount: number }>)[
category
] || { basis: 0, amount: 0 }
cat.basis += sre.avgifter_basis_override ?? sre.avgifter_basis
cat.amount += sre.avgifter_amount_override ?? sre.avgifter_amount
// F-skatt rows contribute 0 regardless of overrides (see isFSkattRow).
cat.basis += isFSkattRow(sre) ? 0 : sre.avgifter_basis_override ?? sre.avgifter_basis
cat.amount += isFSkattRow(sre) ? 0 : sre.avgifter_amount_override ?? sre.avgifter_amount
;(avgifterByCategory as Record<string, { basis: number; amount: number }>)[category] = cat
}
const totalAvgifterAmount = Object.values(avgifterByCategory).reduce(
@@ -446,8 +461,9 @@ export async function generateAgiDeclaration(
const totals: AGITotals = {
totalTax: Math.round(totalTax * 100) / 100,
// F-skatt rows contribute 0 regardless of overrides (see isFSkattRow).
totalAvgifterBasis: activeEmployees.reduce(
(s, e) => s + ((e.avgifter_basis_override ?? e.avgifter_basis) || 0),
(s, e) => s + (isFSkattRow(e) ? 0 : (e.avgifter_basis_override ?? e.avgifter_basis) || 0),
0,
),
totalAvgifterAmount: Math.round(totalAvgifterAmount * 100) / 100,