feat(salary): öresavrundning of net pay to whole kronor (#1609)

* feat(salary): öresavrundning of net pay to whole kronor

Some banks reject salary payment files whose amounts carry öre. New
company_settings.salary_net_rounding toggle (off by default): the engine
rounds each net payout up to the next whole krona, never down, and emits
a derived oresavrundning line item (semesterersattning pattern) that
debits 3740 Öres- och kronutjämning so the salary entry stays balanced.
Gross, tax and avgifter are untouched, so AGI/KU are unaffected. Payment
files (pain.001 + Bankgirot LB) get whole-krona amounts via the rounded
net_salary. Toggle in salary settings; payslip and run detail show the
line item.

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

* fix(salary): keep employer cost on the shared definition; block manual rounding lines

Skeptic findings on the öresavrundning commit: (1) the engine included
netRounding in totalEmployerCost while payslip summary, KPI cards and
lönejournal recompute the figure from stored columns, printing two
different totals on the same payslip; employer cost now stays on the
shared definition and the öre cost is carried by the 3740 ledger line.
(2) 'oresavrundning' is excluded from the line-item create/update
schemas: it is the only item type the booking keeps out of the gross
reconciliation, so a manually created row would structurally unbalance
the salary verifikat.

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

* fix(salary): add the item_type CHECK as NOT VALID, validate separately

Compliance-swarm finding (SOC 2 CC8.1): the CHECK re-add scanned
salary_line_items under the ADD's ACCESS EXCLUSIVE lock. Split per the
house pattern (DECISIONS.md 2026-07-13): 20260813143000 re-adds the
constraint NOT VALID, new 20260813143001 validates it under SHARE UPDATE
EXCLUSIVE in its own transaction. The list is a strict superset of the
previous CHECK, so validation cannot fail. Both files are branch-only,
so editing in place is within the never-modify-shipped rule.

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-14 00:36:33 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 4a9fa5e6c5
commit 4bb0655e4a
18 changed files with 470 additions and 2 deletions
+31
View File
@@ -652,4 +652,35 @@ describe('PUT /api/settings', () => {
'employee_vacation_balances',
])
})
it('accepts the öresavrundning toggle', async () => {
enqueueMany([
{ data: { onboarding_complete: true } }, // oldSettings
{ data: { company_id: 'company-1', salary_net_rounding: true } }, // update result
])
const request = createMockRequest('/api/settings', {
method: 'PUT',
body: { salary_net_rounding: true },
})
const response = await PUT(request, { params: Promise.resolve({}) })
const { status, body } = await parseJsonResponse<{ data: { salary_net_rounding: boolean } }>(response)
expect(status).toBe(200)
expect(body.data.salary_net_rounding).toBe(true)
})
it('rejects a non-boolean öresavrundning value', async () => {
enqueueMany([
{ data: { onboarding_complete: true } }, // oldSettings
])
const request = createMockRequest('/api/settings', {
method: 'PUT',
body: { salary_net_rounding: 'yes' },
})
const response = await PUT(request, { params: Promise.resolve({}) })
expect(response.status).toBe(400)
})
})