W-2 payroll audit — what auto-fixes, what acknowledging does, the pre-payrun checklist

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:

  1. Mark as run — the manual button on the card.
  2. 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.
  3. 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 --force from /Users/sztuczna/pepper with .env sourced. --period=YYYY-MM-DD:YYYY-MM-DD targets a specific period (the only way back to a missed one); --approve-only skips 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

ConstantValueUsed for
TOLERANCE_MIN3 minCross-system time match; contiguous same-day Dripos cards within 3 min are merged before matching
MIN_SHIFT_HRS / MAX_SHIFT_HRS1.0h / 9.5hSHORT_SHIFT / LONG_SHIFT
SCHEDULE_WINDOW240 minMax gap for matching a punch to a scheduled shift
LATE_THRESHOLD15 minLATE warning: clock-in more than 15 min after scheduled start
OVERSTAY_THRESHOLD15 minOVERSTAY flag: clock-out more than 15 min after scheduled end
PAID_DIRECTBrooke CamperoPaid via Mercury directly; excluded from the audit entirely
CATERING_DEPT774235Catering 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_DRIPOSRecord on one side only
SHORT_SHIFT / LONG_SHIFTUnder 1.0h or over 9.5h
UNSCHEDULED (Unscheduled punch)Punch with no schedule that day at all
NO_PAY_EARNEDWorked minutes but Dripos shows $0 — almost always a null ROLE_ID on the card
OVERSTAYClocked 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_HOURSA 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

StatusShown asMeaning
resolvedresolvedAuto-fixed this run; stays listed with its resolution
informationalokNot a data error — a LONG_SHIFT both systems agree on, an explained NO_SHOW
needs_input / openinput neededA real judgment call; blocks approval
acknowledgedacknowledgedMaciej 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:

  1. NO_SHOW / MISSING_7SHIFTS with a valid Dripos card → create the 7shifts punch from the card.
  2. SHORT_SHIFT / LONG_SHIFT → correct whichever side is anomalous from the other side's valid record.
  3. 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.
  4. MISSING_DRIPOS with a valid 7shifts punch → create the Dripos card.
  5. UNSCHEDULED confirmed by a matching Dripos card → resolved (schedule may need updating; pay is fine).
  6. 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).
  7. 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 in data/payroll-audit-reviewed.json under 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 rates list on payroll-data.js#EMPLOYEES (or team_profiles.rate_cafe/rate_catering): the row whose from date 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_TIPS from 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.


Was this article helpful?