This article explains the W-2 side of the Payroll module: the pay cycle it tracks, what the biweekly timeclock audit checks and fixes on its own, what a red flag's status means, what Acknowledge actually does downstream, the pre-payrun checklist, and how the per-employee est. gross figure is built. The panel keeps the short labels and links here under How it works.
The cycle: Tue–Mon, biweekly, Dripos/Check
Pay periods run Tuesday to Monday, two weeks. Payroll runs on the Tuesday after the period closes (runBy); Dripos runs it through Check. The Run payroll card shows the run-by date, the countdown, and the payday.
The authoritative cycle is read from Dripos itself — GET /payroll/history, filtered to type === 'regular' (off-cycle and balancing runs carry their own paydays and would hijack the card). Each run carries period_start, period_end, approval_deadline, payday, approved_at and status (draft → processing → paid); anything past draft means payroll was run, and the card shows that status and the approval date. Three fallbacks, in order:
- Mark as run — the manual button on the card.
- auto-detected — a real Check Payroll debit hitting Mercury between run-by and a few days past payday. Third choice only: Check pulls funding about 3 days before payday (Jul 10 debit → Jul 13 payday; Jul 25 → Jul 28), so it never fires in the window between approving and payday.
- estimated dates — Dripos unreachable; dates come from the historical anchor. Never trust these for a deadline.
The audit script and when it runs
scheduled-tasks/payroll-audit/payroll_compare.py compares 7shifts punches with Dripos time cards for the just-closed period. It runs three ways:
- Scheduled: launchd
com.pepper.payroll-audit, Tuesday 9am CDT, biweekly. The gate is an anchor date of 2026-03-16: it runs when(today − anchor) % 14 == 1. - From the card: Run payroll audit now →
POST /api/payroll/run-audit; the button reads Running audit… (up to 2 min) while it runs. - By hand:
python3 scheduled-tasks/payroll-audit/payroll_compare.py --forcefrom/Users/sztuczna/pepperwith.envsourced.--period=YYYY-MM-DD:YYYY-MM-DDtargets a specific period (the only way back to a missed one);--approve-onlyskips the Telegram report and Dripos prep and just finishes an approval.
Dripos time cards come through the Dripos browser daemon (localhost:7731, launchd com.pepper.dripos-daemon). Give the daemon a few seconds after any restart before running the audit — a race between reconnect and audit corrupted real timecards on 2026-07-24.
Step 0 is the rate audit: scripts/dripos-rate-audit.py asserts every W-2's resolved Dripos rate against payroll-data.js#EMPLOYEES. Exit 0 = match; exit 1 = drift, stop and fix before approving anything; exit 2 = could not run (usually an expired Dripos session). A Dripos role carries several rate definitions and the most recently created one silently wins — role 4456 Catering Barista holds both $27 (1099 L2) and $25 (W-2). Do not fix drift by editing a role-based rate; add a new employee rate so the newest definition is the right one.
What the audit checks
| Constant | Value | Used for |
|---|---|---|
TOLERANCE_MIN | 3 min | Cross-system time match; contiguous same-day Dripos cards within 3 min are merged before matching |
MIN_SHIFT_HRS / MAX_SHIFT_HRS | 1.0h / 9.5h | SHORT_SHIFT / LONG_SHIFT |
SCHEDULE_WINDOW | 240 min | Max gap for matching a punch to a scheduled shift |
LATE_THRESHOLD | 15 min | LATE warning: clock-in more than 15 min after scheduled start |
OVERSTAY_THRESHOLD | 15 min | OVERSTAY flag: clock-out more than 15 min after scheduled end |
PAID_DIRECT | Brooke Campero | Paid via Mercury directly; excluded from the audit entirely |
CATERING_DEPT | 774235 | Catering shifts are off-site with no Dripos terminal; no punch there is normal, not a no-show |
Red flags block approval; warnings do not.
| Red flag (label in the panel) | Meaning |
|---|---|
NO_SHOW (No show) | Scheduled café shift with no punch. Downgraded to informational when 7shifts' own attendance status already explains it (sick, excused) |
MISSING_7SHIFTS / MISSING_DRIPOS | Record on one side only |
SHORT_SHIFT / LONG_SHIFT | Under 1.0h or over 9.5h |
UNSCHEDULED (Unscheduled punch) | Punch with no schedule that day at all |
NO_PAY_EARNED | Worked minutes but Dripos shows $0 — almost always a null ROLE_ID on the card |
OVERSTAY | Clocked out more than 15 min past scheduled end. Always needs_input: an event ran long or they covered someone are legitimate, but it never auto-approves silently (added 2026-07-27) |
CATERING_HOURS | A scheduled catering shift with no punch. Always informational: catering runs off-site with no Dripos terminal, so the hours come from the event record, not a punch. Since 2026-08-19 a catering shift can never produce a NO_SHOW label |
Warnings: LATE, TIME_DIFF, AUTO_CLOCKED_OUT, NO_DURATION, SHORT_SHIFT_CORRECTED, SCHEDULE_MISMATCH. Every LATE and OVERSTAY instance is also tallied per employee (count and minutes) into attendanceSummary in data/payroll-audit-flags.json and appended to data/attendance-history.json; the Team profile shows these. That is performance-review input, separate from payroll correctness.
Flag statuses
| Status | Shown as | Meaning |
|---|---|---|
resolved | resolved | Auto-fixed this run; stays listed with its resolution |
informational | ok | Not a data error — a LONG_SHIFT both systems agree on, an explained NO_SHOW |
needs_input / open | input needed | A real judgment call; blocks approval |
acknowledged | acknowledged | Maciej pressed Acknowledge; no longer blocks |
What auto-fixes, and the gate on it
Classification always runs. The writes are gated behind PAYROLL_AUDIT_AUTOFIX=1, off by default, since a 2026-07-24 bug fix re-armed a silently no-op write path and it overwrote a correct timecard on a test run. With the gate on, the safe fixes are, in order:
NO_SHOW/MISSING_7SHIFTSwith a valid Dripos card → create the 7shifts punch from the card.SHORT_SHIFT/LONG_SHIFT→ correct whichever side is anomalous from the other side's valid record.TIME_DIFF→ the side with a normal duration wins; if both are normal, the clock-in closer to the scheduled start wins; if neither, leave the warning.MISSING_DRIPOSwith a valid 7shifts punch → create the Dripos card.UNSCHEDULEDconfirmed by a matching Dripos card → resolved (schedule may need updating; pay is fine).NO_PAY_EARNED→ assign the employee's own catering role, only when their recent Dripos history shows exactly one catering-prefixed role. Any ambiguity stays with Maciej — the café role on a catering card underpays ($17 vs $25; Nathan 7/7, $38.14 short before it was caught).- Whatever is still open: a LONG_SHIFT with no normal counterpart on either side becomes informational; everything else becomes
needs_input.
Dripos gotcha: its edit endpoints need the numeric ID, not the UNIQUE_ID string; the string form returns HTTP 200 with success: false and does nothing.
What Acknowledge does
Approval is gated on zero open/needs_input flags. Since OVERSTAY landed there has always been at least one, so the period-level Mark period reviewed button (server-enforced, needs zero flags needing input) was unreachable, and two periods (7/21–8/03, 8/04–8/17; 50 punches) sat unapproved and open until 2026-08-19. Per-flag Acknowledge is the release valve:
- It posts to
/api/payroll/audit-flag/acknowledge, recorded indata/payroll-audit-reviewed.jsonunder the period key as(kind, date, name). Undoable. - Acknowledging the last blocking flag makes the backend spawn
payroll_compare.py --approve-only --period=…, which approves every 7shifts punch for the period, approves every Dripos card for the period one PATCH at a time, then closes the 7shifts pay period (PUT /api/v2/company/401989/payroll_period/{id}/close; a 409 on an already-closed period is fine). No waiting for Tuesday. - Never Dripos' blanket approve all unapproved: it is not period-scoped, always has out-of-period cards, and scrambled card fields once (2026-07-24).
- Approving a Dripos card reprices it. Dripos has no approve-only endpoint; approval is a full re-save and it recomputes pay against the employee's current role rate. 2026-08-19: approving 8/04–8/17 zeroed 11 of Javier's cards (82.78h, $1,324.53) because his 8/17 promotion dropped the old role. The script now compares pay before and after each approval and aborts the run if any card reprices down.
- Acknowledging is not resolving. The flag stays on record and in the attendance tallies. A real data error (a wrong clock-in) must be fixed in the source system first — acknowledging it approves the wrong number.
The red-flags panel disappears once the period is reviewed. Its status line (Audit ran … but couldn't finish, Audit passed, prepped in Dripos) reads ok/issue from data/payroll-audit-flags.json, written on every invocation. If the dashboard looks stale after a manual run, compare that file's ranAt first.
Pre-payrun checklist
The Pre-payrun checklist panel lists approved 7shifts time off overlapping the period (person, category, dates, hours). 7shifts-approved time off does not flow into Dripos — Dripos has no API for it — so paid leave must be entered in Dripos by hand before running payroll. The audit's PLAWA step reports the same list to Telegram (Paid Leave — Enter in Dripos Before Running Payroll), skipping only requests whose category is unpaid; anything uncategorised is surfaced for a look rather than dropped. Note this check silently crashed on every run until 2026-08-19 (string-vs-date comparison; wrong field names for API version 2022-05-01).
Running payroll
After a clean audit the script asks the daemon to prep the Dripos payroll page (POST /payroll/prep) and reports whether the Regular Payroll card is clickable; the card then shows Audit passed, prepped in Dripos — review & submit linking to https://dashboard.dripos.com/payroll/overview. Pepper never submits payroll. Where Maciej still steps in: needs_input flags, PLAWA entry in Dripos, submitting the run, and deciding when to turn PAYROLL_AUDIT_AUTOFIX on.
Employees (W-2) · this period: est. gross
Each row shows café hours, catering hours, tips and est. gross · period to date, built as:
- Base = each 7shifts punch's hours × the dated rate for that punch. Punches in department 774235 use the catering rate, everything else the café rate. Rates are the
rateslist onpayroll-data.js#EMPLOYEES(orteam_profiles.rate_cafe/rate_catering): the row whosefromdate is on or before the punch date applies, so a mid-period promotion prices only the punches after its effective date. The L2 {date} tag marks such a change. A W-2 with no rates on file is not costed and a warning is logged. - Tips =
AMOUNT_TIPSfrom the Dripos timecards, period so far. - OT? is a naive weekly-hours check within the period, not a Dripos calculation.
The people table's YTD for W-2s is gross from paid Check runs; in Check payroll means no paid run was found for that name.