Back to catalogue

Payroll run, end to end

Draft a run, compute every payslip, get a different person to approve, pay — then notify each member, all on the immutable trail.

The whole flow, end to end

Mail = email notification Bell = in-app alert (fail-soft) Amber diamond = automatic check Teal diamond = a person decides (four-eyes)
2 · APPROVE & PAY 1 · DRAFT & COMPUTE Maker creates run status: draft · period selected Duplicate active run? UNIQUE entity + period Compute payslips gross = base + commission − deductions Correction needed? salary / commission edit Submit run status: pending_approval · approver emailed Has approve role? CHECKER gate Approver ≠ creator? approved_by <> created_by Mark paid status: paid Members notified payslip: in-app + email (fail-soft) no no yes approved Blocked — duplicate existing non-voided run for period BLOCKED yes Recompute draft re-run figures · re-audit yes recompute 403 Forbidden role actor not in approveRoles no Run voided + reason own audit row · maker notified reject GovernanceError 403 four_eyes — self-approval blocked DEAD END same person then

Self-approval is blocked twice — a code-level GovernanceError and a database CHECK constraint (approver <> created_by). Commission is injected from the commission sub-store; a draft can be recomputed and re-audited before submit. Paying notifies every member fail-soft.

What the payroll run guarantees

  • Every state change — create, recompute, submit, approve, pay, void — writes one immutable audit row via the atomic transition seam (fail-closed).
  • Maker and checker are config-driven roles — no hardcoded role literals; the writeRoles gate makes, the approveRoles gate checks.
  • Paying the run notifies every member with a payslip — fail-soft, so a delivery miss never reverses a recorded payment.