This article is the rulebook behind the Payroll module's Contractors (1099) · owed queue: how a shift gets into the queue, how its hours and amount are derived, what the café↔café and van toggles do, how café weeks pay out, how no-shows are handled, and what each Mercury setup state means. It also covers the read-only W-2 catering punch-check that shares the crew/driver rule. Everything here is what the code does as of 2026-09-01; the panel keeps the short labels and links here under Rules.
What pays, from where
Every Pay button in the queue fires a Mercury ACH from Operations ••9757 (Mercury account c007de6e-f135-11ef-b7b3-d3cff304b4a4) and writes a row to the contractor ledger (Supabase contractor_payments). The server recomputes the amount from source on every pay call — it never trusts an amount sent by the browser — and refuses a second catering record for the same person and date that is already pending, processing or paid.
The payment note the contractor sees reads event, date, shift window, hours × rate — no name (Maciej, 2026-07-24, reconfirmed 2026-08-02). The Nacha externalMemo is MMDDYY PAY.
Rates come from Supabase team_profiles (edited in the Team tab): rate is the catering base, cafeRate the café rate. DEFAULT_CONTRACTOR_RATE ($25, L1 catering) is used only for owed estimates when a person has no roster match.
How a shift enters the owed queue
| Source | Window | What produces the row |
|---|---|---|
| Catering punches | last 21 days | 7shifts punches in the catering department (774235) by non-W-2 users with no ledger row for that date |
| Scheduled catering shifts with no punches | last 21 days | The normal case — contractors rarely punch for events. The 7shifts schedule supplies the start; Bouncie supplies the end |
| Café shifts | completed shifts, up to the contractor's last café day (cafeUntil) | 7shifts café schedule for the person; hours and tips from the Dripos timecard |
| Backup/Interested helpers | events in the last 21 days | manualStaff in data/catering-meta.json — people assigned via the Catering module's + add confirmed helper, who have no 7shifts account |
| Staged rows | until paid | Ledger rows staged by hand — the barista-payment skill or texted hours — with the amount already computed and reviewed. Pay just fires the ACH on that row |
A row leaves the queue when a ledger row exists for that person and date (matched by 7shifts id or, for older rows, by name), when it is paid, or when a no-show is acknowledged. The empty state reads No unpaid contractor events in the last 21 days.
Catering hours: scheduled start + Bouncie van end
For an event shift, the payable window is the scheduled start (unless a text says they were late) to the Bouncie van end. Two ends exist for the same trip:
- crew end — the van departs the venue. This is the end for someone who met on location.
- driver end — the van is back at the café. This is the end for someone who started at the café and rode back.
No punches are needed for this path; the row shows sched+van. Rows with a Bouncie end carry both ends (hours for crew, hoursDriver for café-to-café) and the café↔café toggle picks between them.
No van trip that day usually means the person met on location and someone else drove. The row shows the scheduled window and two actions: Approve pays the scheduled window as-is; Edit opens the start and end times, and Save re-prices the row without paying. A hand-set window stays editable right up until it is paid. A row with no punches, no van trip and no scheduled window cannot be paid until both times are entered.
The café↔café toggle
café↔café means the person's commute that day was café → venue → café, as opposed to meeting on location. On a Bouncie-derived row it decides the payable end: ticked, the window runs to the van's return at the café (later end, more hours); unticked, it stops when the van leaves the venue. It is independent of the van premium — a rider can be café-to-café without driving (Maciej, 2026-07-24).
- Shown only for contractors on track
both(café and catering). A catering-only contractor always meets on location, so the toggle is hidden and the server ignorescafeToCafefor them rather than erroring. - Pre-set from the schedule (
· auto): the person is marked likely driver when their scheduled start is earlier than everyone else's on the event and their scheduled end is last or tied. Tied counts because the 7shifts schedule often gives the whole crew the same placeholder end. The pre-set applies only to people flaggedcanDriveon the roster. - On an approved or hand-set window the toggle is record-keeping only — the entered times are already the worked window.
- Forced on when the van toggle is on. Driving means café-to-café by definition. The checkbox is disabled while the van toggle is ticked, and the server sets
cafeToCafe = truewheneverdriveris set regardless of what the client sent (Maciej, 2026-08-02).
The van toggle
The truck icon is the van/driver premium: +$3/hr on the full shift (DRIVER_PREMIUM = 3, approved 2026-05-12). Rate = roster catering rate + $3. It appears only for contractors with canDrive; the server throws not an authorized driver — no van premium for anyone else. It pre-sets from the same schedule rule as café↔café. Both toggles are overridable, except that café↔café cannot be unticked while the van toggle is ticked.
Since 2026-09-01 the toggles also appear on no-van rows (approve/edit path), so a shift without Bouncie data can still be paid at the driver rate.
Café shifts pay weekly
- One ACH per person per week, Mon–Sun, after the Sunday shift. Rows in an open week show pays after {date}; the last shift of a completed week carries the Pay week button and the other rows point to it (↳ week of {date}).
- Hours and tips come from the Dripos timecard (
AMOUNT_MINUTES,AMOUNT_TIPS). A missing timecard blocks the whole week ({n} timecard(s) missing) until it is fixed in Dripos — the queue never pays a café shift off the schedule. - A departed contractor (their
cafeUntilhas passed) pays out immediately; no more shifts can land in that week. Scheduled café shifts aftercafeUntilare stale rows and are skipped. - A no-show day is dropped from its week rather than blocking it — the rest of the week is real work and still pays on time.
- The single-shift Pay path refuses café rows (Café shifts need the tip-pool share); café pay goes through the week button or the barista-payment skill.
No-shows
| Café shift | Catering shift | |
|---|---|---|
| How it is detected | Automatically from 7shifts: attendance status no_show and no Dripos timecard. Baristas punch for every café shift, so this is trustworthy | Never automatically. Contractors do not punch for events, so 7shifts calls every catering shift a no-show whether they worked it or not. A real one is marked by hand: open the row → Didn't turn up? → Mark no-show |
| Row shows | no-show — nothing owed · from 7shifts | no-show — nothing owed · marked, with the timestamp |
| Undo | Not hand-unmarkable. The fix is the punch or timecard itself — enter it and the row prices and pays normally | Undo — they did work it in the row sheet |
| Where it is stored | derived each sweep; an acknowledgement is recorded in data/payroll-no-shows.json | data/payroll-no-shows.json |
Nothing owed means the Pay path is closed server-side: every pay call runs assertNotNoShow(), which re-reads the file rather than a cached snapshot, so a mark set seconds ago still blocks and a stale tab cannot pay it. Only hand marks block; a 7shifts-derived record un-decides itself the moment a punch and timecard exist.
Acknowledge exists because nothing downstream ever clears a no-show — without it the row sits in the queue forever (Maciej, 2026-08-19). Acknowledging releases the row from the queue and keeps the record; it is not a resolution and never opens a pay path. Released rows are listed, collapsed, under the queue with an Undo. Separately, com.pepper.7shifts-flag-cleanup (daily 7:20am CDT, scripts/7shifts-clear-shift-flags.py --apply) clears the bogus 7shifts flags off future, unassigned and past catering shifts, skipping any shift with a genuine hand mark on file; café flags are left alone.
Row tags and statuses
| Tag / status | Meaning |
|---|---|
café | A café shift — pays weekly, hours and tips from Dripos |
driver | The van toggle is ticked on this row |
staged | Manually staged (barista-payment skill or texted hours); not derived from a schedule |
backup · no 7shifts | Staffed via the Catering module's Backup/Interested list — no 7shifts account, so no punches can ever exist. Hours come from the event's scheduled window (Approve or Edit) when one resolved. With no window and no van data the row carries the tag no 7shifts/van data, its schedule line reads no 7shifts record and no van trip data — hours unknown, and no Approve or Edit is offered. No pay path exists for that row: payOwed() throws No punches and no van trip data for that shift — approve or edit the scheduled time to pay it. Fix: remove and re-add the helper through the Catering module's day picker so a scheduled window resolves (the row then gets Approve/Edit), or stage the payment by hand with the barista-payment skill |
hand-set | Times entered by hand; still editable until paid |
sched+van | Scheduled start + Bouncie van end; no punches needed |
| no van trip — approve sched | No Bouncie trip for this person that day; approve or edit the scheduled window |
| no timecard — fix in Dripos | Café shift with no Dripos timecard; blocks its week |
| needs punches | Catering row with no punches, no van trip and no scheduled window |
Mercury setup states
Assigning a Backup/Interested helper to an event (+ add confirmed helper) runs ensureMercuryRecipient() in pepper-ui/backend/payroll-data.js; the same function runs as a backstop whenever an unmatched name reaches the owed queue. CLI twin for skills: scripts/mercury-ensure-recipient.sh "First Last" [email]. Matching order against existing Mercury recipients: email → full name → last name → unique first-name token; ambiguous names are deliberately left unmatched because this decides where money goes. If nothing matches and an email is on file, it creates the recipient (name + email), stores the id on team_profiles.mercury_recipient_id, and drives Mercury's web UI via scripts/mercury-request-details.py to send both the payment-details request (expires 30 days) and the tax-documents / W-9 request (expires 90 days). If that automation fails it falls back to a Todoist task.
| State (Catering chip / Payroll row) | Meaning | What to do |
|---|---|---|
pending / awaiting bank details | Recipient exists; Mercury is waiting on the contractor to submit bank details. Mercury reports such a recipient active, but an ACH against it cannot settle | Nothing. The Pay button arms itself once Mercury has their account details |
| ✓ / payable | Set up with bank details on file | Pay as soon as hours land |
| no email / no recipient | No email in the Notion Contractor Baristas DB, so no recipient could be created | Add the email in Notion and re-add them to the event, or set them up in Mercury by hand |
| Mercury / Mercury setup failed | The create call failed | Check pepper-ui-err.log; the next payroll sweep retries |
Every pay call also runs assertBankDetails() as a pre-flight; if Mercury itself is unreachable the check is skipped rather than blocking a real payment. Creating a recipient never moves money.
W-2 catering punch-check (read-only)
The Employee catering punch-check panel applies the same crew/driver rule to W-2 staff, whose catering hours must land in both Dripos and 7shifts. Correct start = scheduled start; correct end = Bouncie van return — crew when the van leaves the venue (met on location), driver when it is back at the café (started at café, signalled by a café-Bar POS clock-in). Flagged rows show the estimated uncaptured amount at the catering rate; fix them in Dripos and 7shifts.
Auto-correction of an existing punch is intentionally off: the Dripos write path is not proven, the café-POS signal is not rolled out, and (per Nathan, Jul 8) pay is sometimes already right — a human eyeball beats a blind write. The one automated write is narrower: the nightly auto-filer (pepper-ui/backend/payroll-autofix.js, 21:15 CT, standing authorization Maciej 2026-07-18) files the timecard in both systems only when both are missing, the end is Bouncie-confirmed, the window is in the past and no longer than 14h, then Telegrams what it filed.