Was everyone who should be paid actually paid?
The run finished, the totals tied, and one worker was still not paid. A total cannot catch a person who is missing from it. This control counts both directions: every active worker accounted for in the run, every payment mapped to an active worker.
◆ The failure the money checks cannot seea missing person is invisible to a sum.
Every money control in this module compares two versions of the same population: payroll lines against ledger lines, this month against last month. If a worker fell out of the run entirely, both sides of every comparison are equally short, everything ties, and the first alert is the worker at the door on pay day.
The fix is a count with a roster behind it, not another total. The missing timesheets control does the same job one step earlier, for hours; this one does it for the payments themselves.
◆ Forward: every active worker accounted forthe May pay date, 351 people, three buckets.
| May 2026 pay date | Workers |
|---|---|
| In the May runs and paid | 348 |
| On approved unpaid leave, no pay due | 2 |
| In no run at all, the finding | 1 |
| Active workers on the pay date | 351 |
The 351 is the same population the headcount report counts, and the two on leave reconcile to the workers on leave report. Every active worker lands in exactly one bucket, so the one who fell through has a row instead of an absence. Counts are illustrative, never client data.
◆ Reverse: every payment mapped to a workerthe same check, pointed the other way.
| May 2026 payments | Workers |
|---|---|
| Paid and active on the pay date | 348 |
| Paid but no longer active, final pay | 2 |
| Workers paid in the May runs | 350 |
The reverse direction catches the opposite failure: money leaving for someone who should no longer receive it. Here the two are April leavers receiving a legitimate final payment. When a name shows up in this bucket without a matching termination, that is the payment to stop before it recurs.
◆ The owned answertwo outer joins, run on a schedule, exceptions kept.
The build lands the run membership beside the worker roster and runs both directions as one query after every run. Exceptions are stored with the run they belong to, so the answer to when did we last miss someone is a table, not a memory.
-- Both directions in one pass, kept per run
SELECT 'active, not in the run' AS direction, w.worker_id
FROM dim_worker w
LEFT JOIN stg_pay_run_worker r ON r.worker_id = w.worker_id
WHERE w.active_on_pay_date = 1 AND w.on_unpaid_leave = 0
AND r.worker_id IS NULL
UNION ALL
SELECT 'paid, not active', r.worker_id
FROM stg_pay_run_worker r
LEFT JOIN dim_worker w ON w.worker_id = r.worker_id
WHERE w.worker_id IS NULL OR w.active_on_pay_date = 0
Zero rows is the good result, and it is stored too. A control that only speaks up on failure cannot prove it ran.
- completeness
- Proof that everyone who belongs in a process is actually in it.
- pay run
- One execution of payroll; a worker can be in several runs a period.
- run membership
- The roster of workers a run actually picked up and paid.
- active worker
- A worker employed and in pay status on the pay date.
- unpaid leave
- An approved absence with no payment due, excluded on purpose.
- final pay
- A leaver's last payment, legitimately after their end date.
- exception
- A worker on one side of the reconciliation with no match on the other.
- both directions
- Roster to run and run to roster; each catches what the other cannot.