fix(salary): declare, book and pay AGI in whole kronor (SKV per-sats computation) (#1611)

* fix(salary): declare, book and pay AGI in whole kronor (SKV per-sats computation)

A user's first lönekörning surfaced öre amounts in the AGI payable while
Skatteverket deals in whole kronor. Three connected defects:

- the AGI XML rounded amounts (Math.round); öretal bortfaller (SFF
  2011:1261 22 kap. 1 §) requires truncation, and FK487 must be
  Skatteverket's own per-sats computation on the whole-krona underlag sums
  (IK587, kontroll B_006), not a truncation of the öre-exact engine sum
- the salary booking credited 2731 with exact öre, leaving a residual
  after the whole-krona skattekonto draw; 2731 now carries the declared
  amount with the remainder on 3740 (Öres- och kronutjämning)
- the LB payment file and TaxPaymentPanel paid/showed öre; they now use
  the declared whole-krona totals stored on agi_declarations (which also
  lets skattekonto auto-settlement match the draw); legacy öre rows keep
  paying öre-exact so pre-deploy bookings still clear 2731

New lib/salary/declared-avgifter.ts implements the SKV computation (per-IU
whole-krona underlag, per-sats sums, youth/växa cap splits, exact integer
math) shared by the AGI generator, the booking split and the preview.
Review overrides route all legs through the same per-category truncation;
basis overrides are inert on money totals (they never reach the filed
IUs); the v1 book route gains override parity with book-run; F-skatt rows
ignore avgifter overrides on every surface. Booked runs show their posted
verifikat instead of a recomputed projection. tax_withheld_override
requires whole kronor. Adversarially verified over three /skeptic rounds.

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

* chore: merge origin/main and re-ratchet the öre-round baseline

The merge brought #1609 (net-pay öresavrundning) whose two new
Math.round(x*100)/100 occurrences are counted against the baseline this
branch had tightened from 637 to 629; 631 keeps the net -6 improvement
without policing already-merged code.

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

* fix(salary): address PR review (hybrid override computation, legacy youth cap, robustness)

CodeRabbit round on #1611, all findings in one pass:

- computeDeclaredAvgifterWithOverrides: one shared hybrid for the AGI
  generator AND the booking split. Overridden rows contribute their manual
  amounts per category; colleagues keep the SKV-exact per-sats underlag
  computation (a FoU override on one employee no longer costs the rest of
  the roster kronor of declared accuracy)
- youth cap keys on the RESOLVED category so legacy null-category rows
  classified as youth by the rate heuristic still get the 25k split
- F-skatt rows zero their avgifter_basis on both booking surfaces and in
  the preview, matching the AGI's isFSkattRow invariant
- preview route: posted-voucher lookup errors return 500 instead of
  masquerading as a booked run with no vouchers; 400/500 tests added
- run page clears stale AGI totals when the tax-payment fetch fails
- SalaryOverridePanel truncates the tax override to whole kronor so the
  schema's .int() cannot bounce a decimal input with a 400
- v1 book route override parity pinned by a lifecycle test
- DECISIONS.md format fixes + superseded entry marked; exempt category
  mapped explicitly; unified truncation-drift band with rationale

Declined (recorded): dating the decision entries 2026-08-13 (bot assumed
UTC; the decisions were made after midnight local time).

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

* fix(salary): round-2 review nits (shared F-skatt helper, test hygiene)

- isFSkattStatus in declared-avgifter.ts: single source for the F-skatt
  exclusion, consumed by book-run, the v1 book route, the preview route and
  the AGI generator, per the Swedish review's drift-risk finding
- declared-avgifter test suite gets the standard beforeEach cleanup

Declined (recorded for the summary): auto-generated correction voucher for
regenerated legacy periods (data-repair follow-up needing Emil's go); SFF
22 kap. 1 par. citation doubt (verified against lagen.nu and already shipped
in tax-tables.ts); 3740 scope doubt (BAS generic utjamning account, Visma
praxis, matches the user's reference voucher).

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 02:22:07 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent fbe4e18730
commit 4e14182a00
28 changed files with 1642 additions and 102 deletions
+10
View File
@@ -956,6 +956,16 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-13] PR #1598, compliance findings closed with the rollout after the Swedish accounting review escalated them from follow-up to fix-with-rollout: (a) runReconciliation's >= 0.9 auto-apply now writes 'matched' to payment_match_log (behandlingshistorik, BFNAR 2013:2 kap 8); (b) the three match-route storno-conflict branches no longer storno-reverse a reconciliation-linked verifikat: a reconciliation link points at an independent verifikat that may evidence other affarshandelser, and a wholesale reversal is an over-broad rattelse (BFL 5 kap 5 §). The detach is DEFERRED (round 2, CodeRabbit): nothing is persisted up front; the final transaction update overwrites the pointer and clears reconciliation_method in the same write, so a failure anywhere in the match flow leaves the existing link intact, and the release is logged as 'unmatched' after the commit.
[2026-08-13] PR #1598, CodeRabbit findings: confirm-suggestions maxDuration 300; lookbackTouched on the migrator nudge buttons; persistSuggestions on main's post-backfill sweep; sie_sweep stamp errors logged; sandbox keeps the CSV CTA (file import works there); payment_match_log CHECK swap now NOT VALID + VALIDATE (no table-scan under ACCESS EXCLUSIVE); every logMatchEvent call awaited (serverless can freeze unawaited work).
[2026-08-13] Historical audit gap quantified on prod (read-only): 762 manual-method links across 52 companies since 2026-03-23 have no payment_match_log row (upper bound: includes linked_to_existing_voucher drops AND older unlogged manual paths). Not backfillable (the inserts never landed); the links themselves are intact on transactions. Recorded here as the explicit ops note the compliance review asked for.
[2026-08-14] AGI whole-krona handling is truncation (oretal bortfaller, SFF 22 kap. 1 par.), never rounding, with the remainder credited to 3740 in the avgifter voucher, Visma-style: rounding inside the salary run (Dooer-style) was declined because it distorts the exact 7510 cost and per-employee amounts.
[2026-08-14] FK487/2731/payment amount follows Skatteverket's own computation (IK587): per avgiftssats on the whole-krona underlag sums, with youth/vaxa cap splits, in lib/salary/declared-avgifter.ts. First attempt truncated the engine's ore-exact sum; the skeptic pass refuted it (diverges kronor from SKV's draw on multi-employee ore rosters). Two accepted 1-kr-scale approximations documented in the module header: trunc(basis) instead of per-FIELD truncation, and per-(category,sats)-cell truncation so the category breakdown cross-foots exactly. Amount overrides (FoU-avdrag) fall back to per-category truncation of the overridden amounts.
[2026-08-14] agi_declarations.total_tax/total_avgifter store the DECLARED whole-krona amounts (was: ore-exact run totals). The payment file pays those for whole-krona rows but keeps paying ore-exact for legacy ore rows: their salary bookings credited 2731 with ore, and truncating the payment would strand the ore on 2731 (skeptic regression finding R3).
[2026-08-14] tax_withheld_override requires whole kronor (schema .int()): an ore override would book 2710 with ore the whole-krona skattekonto draw never clears.
[2026-08-14] SUPERSEDED (same day, by the booked-verifikat entry below): journal preview hidden for booked/corrected salary runs: a preview built by today's booking rules would contradict immutable verifikat booked under earlier rules (skeptic regression finding R2).
[2026-08-14] Amount overrides route all three legs (booked 2731, stored/filed FK487, payment) through the SAME hybrid computation (overridden rows contribute their manual amounts per category; colleagues keep the SKV-exact underlag computation); basis-only overrides are inert on money totals: a basis override never reaches the filed IU fields, so letting it steer FK487/payment would file an FK487 contradicting the declaration's own IUs and underpay the skattekonto (skeptic round-2 finding). The booking detects amount overrides via an explicit flag, never the magnitude band, so a small deliberate adjustment cannot book as fake oresutjamning.
[2026-08-14] No .int() guard on avgifter_amount_override/avgifter_basis_override (unlike tax_withheld_override): the override editors prefill ore-exact effective values (feedback: prefill override editors), so rejecting ore would 400 a saved prefill; the per-category truncation absorbs ore consistently on all legs instead.
[2026-08-14] Regenerating a legacy-booked period's AGI post-deploy flips its stored totals to declared integers, so the payment then leaves the old booking's ore residual visible on 2731 (skeptic finding F3): ACCEPTED. The alternative preserved a false equilibrium (overpaying skattekontot by the ore forever); the residual is real, one-time, and manually clearable against 3740.
[2026-08-14] Booked/corrected salary runs show the ACTUAL posted verifikat (via the preview route) instead of a recomputed projection: today's booking rules would contradict immutable vouchers booked under earlier rules (skeptic finding R2/F2: gating the preview away entirely removed the page's only voucher view).
[2026-08-14] v1 REST book route gets full override parity with book-run (tax/net/avgifter coalescing + the override flag): the skeptic showed a run with review overrides booked via v1 would diverge from the AGI by the full override delta. F-skatt rows ignore avgifter overrides on BOTH booking surfaces, mirroring the AGI's isFSkattRow invariant (an override on an F-skatt row must never book social charges the declaration excludes).
[2026-08-13] MCP account_override (custom accounts via categorize) implemented as a new shared helper lib/bookkeeping/account-override.ts used by the MCP staging preview and categorize-core commit path, mirroring v1 REST semantics (business-side replacement, class-2 VAT drop with 2610-2649 exception) plus a same-account degenerate guard v1 lacks; the v1/internal REST routes keep their inline copies untouched: refactoring them into the helper would widen a scoped fix into a three-surface regression risk. Divergence from REST: MCP rejects account_override + category 'private' explicitly instead of silently ignoring it (agent surfaces get deterministic errors, not silent drops).
[2026-08-13] gnubok_create_voucher staging gate softened to engine semantics inline (already-fetched chart rows + getBASReference) instead of calling findUnresolvableAccounts: identical verdicts, zero extra queries, and the preview gains will_activate_accounts + BAS-name fallback so the approver sees the auto-activation side-effect.
[2026-08-13] suggest_categories rules on accounts outside the fixed maps now surface as expense_other/income_other with the rule's own account and a neutral Swedish match_reason (no MCP jargon: the same suggestion renders in the web UI); previously such rules were silently dropped (agent-reported as "4020 not available").