Contractor pay rules — owed queue, café↔café, van premium, no-shows, Mercury setup

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

SourceWindowWhat produces the row
Catering puncheslast 21 days7shifts punches in the catering department (774235) by non-W-2 users with no ledger row for that date
Scheduled catering shifts with no puncheslast 21 daysThe normal case — contractors rarely punch for events. The 7shifts schedule supplies the start; Bouncie supplies the end
Café shiftscompleted 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 helpersevents in the last 21 daysmanualStaff in data/catering-meta.json — people assigned via the Catering module's + add confirmed helper, who have no 7shifts account
Staged rowsuntil paidLedger 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 ignores cafeToCafe for 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 flagged canDrive on 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 = true whenever driver is 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 cafeUntil has passed) pays out immediately; no more shifts can land in that week. Scheduled café shifts after cafeUntil are 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é shiftCatering shift
How it is detectedAutomatically from 7shifts: attendance status no_show and no Dripos timecard. Baristas punch for every café shift, so this is trustworthyNever 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 showsno-show — nothing owed · from 7shiftsno-show — nothing owed · marked, with the timestamp
UndoNot hand-unmarkable. The fix is the punch or timecard itself — enter it and the row prices and pays normallyUndo — they did work it in the row sheet
Where it is storedderived each sweep; an acknowledgement is recorded in data/payroll-no-shows.jsondata/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 / statusMeaning
caféA café shift — pays weekly, hours and tips from Dripos
driverThe van toggle is ticked on this row
stagedManually staged (barista-payment skill or texted hours); not derived from a schedule
backup · no 7shiftsStaffed 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-setTimes entered by hand; still editable until paid
sched+vanScheduled start + Bouncie van end; no punches needed
no van trip — approve schedNo Bouncie trip for this person that day; approve or edit the scheduled window
no timecard — fix in DriposCafé shift with no Dripos timecard; blocks its week
needs punchesCatering 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)MeaningWhat to do
pending / awaiting bank detailsRecipient exists; Mercury is waiting on the contractor to submit bank details. Mercury reports such a recipient active, but an ACH against it cannot settleNothing. The Pay button arms itself once Mercury has their account details
✓ / payableSet up with bank details on filePay as soon as hours land
no email / no recipientNo email in the Notion Contractor Baristas DB, so no recipient could be createdAdd the email in Notion and re-add them to the event, or set them up in Mercury by hand
Mercury / Mercury setup failedThe create call failedCheck 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.


Was this article helpful?