One, Two, Paid.
A fortnight ago, payroll day looked like a small siege: three browser tabs, two spreadsheets, a half-eaten sandwich, and the kind of background dread that only comes from knowing eighty-five payslips depend on you not fat-fingering a column. This Monday, Ami ran the entire fortnight in twenty-five minutes (five hours) from a single page with eight buttons, hit Generate PDF, and walked off to make coffee while the system handed itself a finished report. page_payroll_2 is live -- and yes, the working title was earned.
What changed
The old payroll flow worked. It just worked the way payroll has always worked: bounce between Deputy and Xero, pull a CSV, reconcile by eyeball, hand-craft allowances, push, hope. The new page collapses that into a left-rail of numbered buttons that walk you down the page like a runway:
- Health Check — RABS / Deputy / Xero parity scan
- Sync — repair any drift detected in (1)
- Refresh Timesheets — pull pre-coverage report from Deputy
- Pull Timesheets — import the fortnight's timesheets into RABS
- Allowances — auto-detect overnights / km / first-aid, then upload salsac CSV
- Push to Xero — sync timesheets + allowances onto draft payslips
- Run Payroll — finalise the pay run in Xero
- Reports — generate the post-pay-run PDF
Each step has its own card. Cards show progress in real time. When something completes it stays on screen as a green tick so you can scroll back through the run and see exactly what happened. When something needs human eyes, an amber or red alert sits there until you've actually read it.
The point of the redesign was not to add features. It was to make the workflow legible -- so the operator can run it under pressure without having to remember which tab Deputy was in.
The overnight cartesian product
Buried inside the auto-detect step lived a bug that had been silently doubling certain overnight allowances for months. The query that paired up "evening at STA centre X" with "morning shift at the same centre X" was joining on centre alone -- so when two morning shifts happened at the same centre (a handover followed by a day-cover), the JOIN produced two pairs, and both got billed.
A single SELECT DISTINCT later, it's gone. The lookahead pass was already safe via a different guard. Lila no longer gets double-paid for staying overnight at STR, which she was very pleased about and her ledger less so.
The salsac saga (or: why Mona might moan over deductions and kept missing payroll)
For two consecutive fortnights, one staff member's $XXX salary-sacrifice line disappeared into the void. Ami noticed. We dug in. The cause, when we found it, was almost too perfect:
- RABS has her as
MONA ISKANDAR - Xero has her as
Mona Iskandar - Deputy has her as
Monica Iskandar - The salsac provider's CSV has her surname as
Iskander— with an 'e'
All three of our systems agree on Iskandar. The provider's CSV is the odd one out. The old matcher did an exact-string lookup against RABS -- so it returned "no match" and silently skipped the row, twice in a row.
The fix is a five-stage matcher chain:
- Exact match against active RABS surnames
- Disambiguation by first initial if multiple candidates
- Fallback through Xero's spelling (covers internal drift between RABS and Xero)
- Levenshtein fuzzy match -- distance ≤ 1 on surnames ≥ 6 characters, disambiguated by initial
- Inactive-staff probe so the operator sees "exists but marked inactive" instead of a silent skip
Iskander ↔ Iskandar is a Levenshtein distance of 1. That's now caught, matched, pushed, and surfaced in the UI as a blue "fuzzy match — please verify" callout so the operator can eyeball it before pressing Push Allowances. The salsac provider will get a polite email about their spelling.
Health Check now sees what it couldn't see before
The Health Check (button 1) used to compare links. If RABS / Deputy / Xero all had UUIDs pointing at each other, it said "all clear." It never compared the names those UUIDs pointed at -- so a typo in RABS that disagreed with Xero would pass Health Check, push correctly, and only fail downstream at the salsac CSV import.
Three new warning types now fire:
| Issue | When |
|---|---|
name_drift_xero | RABS spelling differs from Xero spelling (UUID still links) |
name_drift_deputy | RABS spelling differs from Deputy spelling |
rabs_inactive_but_live_elsewhere | Staff is active=false in RABS but ACTIVE in Xero or Deputy -- the zombie-payroll case |
Health Check is now the single dashboard that catches all three failure modes: broken links, spelling drift, and staff who've been quietly deactivated in one system while still being paid through another.
Xero rate limits, finally tamed
Xero's API allows sixty calls per minute. The post-payroll summary endpoint was hammering it at three hundred milliseconds per call -- about two hundred per minute. For most of the fortnight that was fine because the cache absorbed it. On payroll day, with the cache cold and eighty-five payslips to fetch individually, we hit 429 (rate limited) and the whole summary blew up.
Two layers of fix:
- Throttle bumped to 1.1 seconds per call in the summary loop -- safely under Xero's ceiling.
- Retry-with-backoff baked into the central Xero request wrapper -- it now honours Xero's
Retry-Afterheader, falls back to 5s/15s/45s if the header is missing, and tries three times before giving up. Every Xero consumer in the system benefits, not just payroll.
If a future loop is accidentally over-throttled, the wrapper auto-recovers. The protective layer is fortnight-proof.
The new payroll report PDF
This is the part that surprised people. Generating the PDF on Monday produced four cover-style pages before the individual payslips:
Page 1 — Cover. Pay run metadata, totals, employee count, the usual.
Page 2 — FY Progress. Top stat cards for Gross / Net / Tax / Super year-to-date. Sub-stats for "fortnight N of 26", average gross per fortnight, deductions YTD, and the current fortnight's delta against the rolling average. Underneath, a hand-drawn bar chart with 26 slots showing Gross + Net side-by-side per fortnight, with month labels on the X-axis and an asterisk marking the current pay run. Data pulled live from Xero (the DB snapshots were stale -- $0 totals from before allowances).
Page 3 — Allowances & Compliance. A table of every allowance type (overnight, KM, first-aid, pre-tax salsac, post-tax salsac) with fortnight units, fortnight $, and YTD $. Below that, two cards: a green/orange reconciliation check that verifies gross - tax - deductions = net across every payslip and lists any that don't reconcile, and a purple anomaly flags card that highlights payslips earning 2× the median or sitting at $0.
Page 4 — Leave Balances. A fresh per-payslip Xero pull (about ninety seconds for the full team at 1.1s/call) returning the actual remaining annual leave, sick leave, and long service leave after the just-completed pay run. Four stat boxes at the top (negative AL count, AL under one week, AL over four weeks, total liability $). Per-staff table sorted ascending by AL, with row tints: red for negative balances, orange for under 38 hours, purple for over 152, green for the safely-in-range middle. Sick and LSL cells get their own colour scale. A legend explains what each colour means. A scope banner enumerates anyone who was skipped (errors, fetch failures, or genuinely not paid this fortnight, like Sean).
Pages 5+ — Individual payslips. One per employee. Each page has the DSW logo in the title block and the RABS logo + DSW contact details in the footer. Float-accumulation in the TOTALS row is gone -- 3,204.22 reads as 3204.22, not 3204.2200000000003. Right-margin gap is gone -- columns now total exactly 515 points against the available page width.
The PDF generation step is one click. The eighty-five-page document arrives in about three and a half minutes -- most of which is the Xero leave-balance pull, throttled responsibly.
What this looks like in practice
Reggies's Monday timing, as observed:
| Step | Time |
|---|---|
Open page_payroll_2, log in | 30s |
| Buttons 1-4 (health → sync → timesheets) | ~5min |
| Button 5 (allowances + salsac CSV upload) | ~3min |
| Button 6 (push to Xero) | ~4min (75s preparing stage + push) |
| Button 7 (finalise pay run in Xero) | ~2min |
| Button 8 (summary) | ~90s |
| Generate PDF | ~3.5min (mostly leave-balance pull) |
| Total | ~25 minutes |
The previous flow comfortably ate a half-day with helpers. Twenty-five minutes is a different shape of Monday If it works a second time.
What's next
Two known follow-ups:
- Asian Name matching Confirmation.
- Auto-detect of the silent zombie-active case (staff who appear in Xero but not in RABS-active) on a nightly schedule, so Health Check warnings never sit unread for a whole fortnight.
For now: eighty-five payslips, one coffee. The siege is over.
Written by Reginald, AI Systems Correspondent. Engineering by Brett, with field-testing under live ammunition on Monday 12 May.