Skip to main content

Retroactive adjustments (TWK)

Real payroll inputs change after a period is sealed — a backdated raise, a late-corrected sick day, a retroactive contract fix. HRMQ settles these the Dutch way: terugwerkende kracht herrekening (TWK). A PayrollAdjustment models the correction as a delta, never as a rewrite of the sealed original.

The sealed original is never mutated

RetroAdjustmentService reads the stored, already-approved (or posted/paid) Payslip only to diff against it — it recomputes the original period with the corrected input and subtracts. It writes a new PayrollAdjustment carrying cents-exact delta* fields (deltaGross, deltaLoonheffing, deltaNet, deltaWerknemersverzekeringen, deltaZvw, deltaVolksverzekeringen, deltaVakantiegeldReserved). The historical payslip — already filed in a loonaangifte, already posted to the GL — stays byte-untouched. An adjustment can exist against a run the engine itself refuses to recompute directly.

An adjustment can only be created against a sealed original run (approved/posted/paid). A still-draft original is corrected directly via hrmq:payroll:run --recalculate, the ordinary engine path — attempting an adjustment against a draft original is refused (refused-original-draft).

The recompute uses the ORIGINAL period's tax year

A 2025 correction must use the 2025 schijven/kortingen/premiepercentages, not 2026. RetroAdjustmentService derives nl-{year of the original period} and recomputes strictly against that table.

Same-tax-year MVP boundary: today only nl-2026.json ships. A correction whose original period falls in a year for which no table exists is refused with a clear historical-tables-missing message naming the missing file, rather than silently recomputed against the wrong year. Seeding historical nl-YYYY.json corpora is a named follow-up — the recompute code is already year-generic, so that follow-up is data, not logic.

The delta surfaces in the CURRENT run, never in history

An applied adjustment folds its net delta into the current open period's Payslip — a new retroAdjustment component field plus its effect on nettoPay — as a nabetaling (positive) or terugvordering (negative). A draft adjustment is computed-but-unsettled and affects no run.

Adjustments are idempotent by (originalPeriod, employeeId, correctionRef) — re-running the same correction updates one object and never double-counts.

The tax year is period-derived and immutable once stamped

There is deliberately no mutable "active tax year" global: each run derives its table from its own period, and a generated run's engineVersion/calculatedAt stamp plus the non-draft recompute refusal make that stamp immutable. The annual roll is therefore data-only — ship lib/Standards/tables/nl-YYYY.json and runs for YYYY-MM periods pick it up automatically.

occ hrmq:payroll:year-transition --year 2027

This is the preflight for the annual roll: it asserts the new table exists (failing loudly otherwise) and confirms the immutable-stamp guard — it changes no engine state.

Known MVP limitations

  • No bijzonder tarief on the nabetaling. A nabetaling is often taxed at bijzonder tarief in real Dutch payroll; the MVP settles the delta at the tabel result and names bijzonder tarief as a follow-up — it inherits the engine's own bijzonder-tarief non-goal.
  • No cumulative/VCR reconciliation — the delta is a period-vs-period recompute, not a cumulative one.
  • No automated loonaangifte correctie-berichten. The filing lifecycle's corrigeren transition is the manual route.

The nl-retro-adjustment-consistency corpus rule recomputes every adjustment's delta during the standing audit, so a tampered delta is a mandatory violation:

occ hrmq:rules:audit

Booking a correction

occ hrmq:payroll:adjust --original-period 2026-02 --employee EID \
--correction-ref t1 --gross 4000 --settlement-period 2026-04 --apply

Or from the UI: PayrollRunDetail for the sealed original run carries a "Correctie boeken (TWK)" action that opens a draft PayrollAdjustment prefilled with the run reference; its own detail page carries a "Herrekenen" action that computes the delta. settle/ apply is deliberately never a bare lifecycle button — both paths resolve the target through the caller's ambient RBAC before any recompute, so an unauthorized or unknown adjustment id never reaches the engine.