Pay & Job Costing module

Retires the Charles-pay spreadsheets. Square-paid jobs → owner enters actual per-job costs → the system computes true gross profit, margin %, and Charles's recommended pay (profit-share above break-even), nets cash advances, and produces the payroll HOURS number + a Square-verified month close. Owner only — the field role (Charles) must never see any of it.

Files

Storage (no new tables)

Model

Acceptance — June 2026 (Square-validated, settled)

npm run test:pay and the live /admin/pay/ (Month Close) both reproduce EXACTLY: billed 9,266 · costs 20 · GP 9,246 · overhead 2,462.13 · model comp 2,713.55 · advances 900 · total comp 2,980 · payroll due 2,080 = 52.00 hrs · pre-tax 3,803.87 · margin 41.1%.

Month bucketing

pay.pay_month (explicit owner anchor — e.g. a July-1 job counted in June) wins; else the service month derived from completed/scheduled/created in America/New_York. Backfill/boundary jobs should carry an explicit pay_month (see cutover SQL) — Karen Hart completed 2026-07-01 but belongs to June's close.

Verified on staging (project flvklgxqebipahilrnln)

Owner login → June reproduces exactly; field login → 403; no-auth → 401; cost add/remove, advance, override, overhead, dials, close/reopen all round-trip. Closed months are read-only in the UI (can't drift).

Prod cutover (owner: desktop-Claude / Ryan)

  1. Run supabase/pay-prod-cutover.sql (anchors 15 June jobs, canonicalizes advances, records the let-ride override).
  2. Deploy pay-data to prod (verify_jwt=true).
  3. /admin/pay/ already defaults to prod. ?env=staging switches to staging for testing.

Open flags for Ryan

  1. Charles's prod role is owner, not field. The auth gate is airtight, but as an owner he'd see his own pay. Fix: update profiles set role='field' where full_name='Charles'; (owner's call).
  2. jobs.pay is column-readable by field via direct PostgREST (RLS is row-level, both roles are authenticated). The Pay UI + edge function are owner-only, but a field user hitting the REST API directly could read the raw column. True hardening needs a redacting view or moving pay to an owner-only table (spec says no new tables) — recommend as a follow-up.
  3. Deal-drawer / job-detail quick-add lives in the separate operator-app repo (not this one). The Pay page already offers per-job cost + advance quick-add; wiring the same into the deal drawer + the "record manual payment" pipeline (advance prompt) is a cross-repo follow-up.