fix(salary): book lonevaxling pension provision from frozen run snapshot (#1382)

The book flow never populated pension_contribution/pension_slp, so a
gross_deduction_pension line item reduced the salary entry but the
7410/2740 pension provision and 7533/2514 SLP lines were never posted.
Derive both at the createSalaryRunEntries boundary from the run's frozen
calculation_params.slpRate snapshot so the dashboard, MCP and v1 booking
paths all emit the pension verifikat, and reuse the exact 1.058 factor
via calculateLoneVaxlingPensionProvision shared with the planning
calculator.

Fixes #317

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-03 17:26:49 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 6782da3e9e
commit c9625fa45c
8 changed files with 143 additions and 13 deletions
@@ -421,6 +421,7 @@ describe('POST /salary-runs/:id/book', () => {
total_net: 25500,
total_avgifter: 10997,
total_vacation_accrual: 0,
calculation_params: { slpRate: 0.2426 },
}
const employeeRow = {
@@ -478,6 +479,12 @@ describe('POST /salary-runs/:id/book', () => {
expect(body.data.entry_ids).toEqual(['je_salary', 'je_avg'])
expect(body.meta.audit.voucher_number).toBe('L2026-0023')
expect(body.meta.audit.voucher_url).toContain('je_salary')
expect(mocks.createSalaryRunEntries).toHaveBeenCalledWith(
expect.anything(),
COMPANY_ID,
USER_ID,
expect.objectContaining({ calculation_params: { slpRate: 0.2426 } }),
)
})
it('returns PERIOD_LOCKED before invoking the engine when payment_date is locked', async () => {
@@ -190,8 +190,16 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
'salary (gross + tax withholding + net payment)',
'arbetsgivaravgifter',
...(totalVacation > 0 || totalVacationAvgifter > 0 ? ['vacation accrual'] : []),
// Pension cannot be detected without the engine: we'd need
// to inspect line_items for löneväxling. Omitted from preview.
...((employees as Array<{ line_items: Array<{ item_type: string; amount: number }> | null }>).some(
(employee) =>
(employee.line_items || []).some(
(line) =>
line.item_type === 'gross_deduction_pension' &&
Math.round(Math.abs(line.amount) * 100) !== 0,
),
)
? ['pension provision and SLP']
: []),
],
note: 'A live call posts 2-4 verifikationer atomically via createSalaryRunEntries. Voucher numbers are assigned at commit time.',
},
@@ -234,6 +242,7 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string
total_net: (run as { total_net: number }).total_net,
total_avgifter: (run as { total_avgifter: number }).total_avgifter,
total_vacation_accrual: (run as { total_vacation_accrual: number }).total_vacation_accrual,
calculation_params: (run as { calculation_params: Record<string, unknown> | null }).calculation_params,
employees: (employees as EmpRow[]).map((sre) => ({
employee_id: sre.employee_id,
employment_type: sre.employee?.employment_type || 'employee',