Adversarial review: weekly billing run, coverage gaps, end-to-end process, AR — 2026-08-14
Findings-only pass (no code changes). Reviewed against the shipped implementation on main
(as of c79f5fa), plus read-only prod queries (project dksjqoknmwpxfqkiygkb) run 2026-08-14.
Scope corrections vs. the review brief. The brief references artifacts that do not exist in
this repo or its git history: INVOICING-HANDOFF.md, the four mockups
(invoicing-tab-weekly.html, generate-from-time-flow.html, review-and-send.html,
hours-override.html), and billing_basis / override_reason columns. The real system has no
free-set "billing basis override" at all — billed hours are always derived:
min(logged, contracted) with the cap lifted only by an approved approval_requests row
(lib/invoice/index.ts:82). The review therefore targets the real
override surfaces (overage approval/waiver, hours_reason, Log-hours upsert, Correct/recompute,
reissue) and, for Part E, the shipped UI
(src/components/admin/invoicing/) instead of mockups.
Severity legend (Parts A/B/D): SILENT = wrong invoice or uncollected money with no signal · CAUGHT = fails loudly / a human would catch it · COSMETIC = non-financial.
Part A — Billing-basis edge cases
KEEP
- A-K1 — Over-contract billing is enforced by arithmetic, not workflow.
EXISTSThere is no "approval checkbox the send forgot to check": without an approvedapproval_requestsrow for that week, over-contract hours are simply not billed (lib/invoice/index.ts:82-86, generate.ts:269-308). An over-contract bill can only exist through an approval. Direct-RPC bypass is closed:send_invoices/create_facility_invoiceare revoked fromauthenticated(0097:141-142), and 0102/0104 closed the PostgREST write path totime_entriesand invoice money columns. Don't "fix" this by adding a workflow gate — the arithmetic is the gate. - A-K2 — The two-actors / two-shelves races are closed.
EXISTSSend: per-invoice row lock + status recheck in the RPC (0097:50-69). Generate: 0087 overlap-aware dedup + unique constraint, surfaced as "already exists" (generate.ts:229-248, 518-526). Send-time staleness guard refuses any draft whose stored total ≠ live recompute (send.ts:420-497). Overage approval is a single-use CAS claim bound to a ±0.01h snapshot of the hours the facility actually saw (overage.ts:238-271). - A-K3 — PHP/FX cannot corrupt an invoice.
EXISTSBilling is USD-native end to end (assignmenthourly_rate_cents, integer cents everywhere). FX exists only on the payroll side (payments.exchange_rate_used, 0002:364), locked per payout at Wise-quote time. A rate-assumption change is a payroll-margin question, never an invoice-correctness one. The brief's premise doesn't apply — worth recording so nobody "fixes" it. - A-K4 — Under-contract billing requires an audited reason.
EXISTSrequireReasons+ per-linehours_reason(0075), or a recorded blanket confirmation from the cockpit batch path (generate.ts:459-469, invoicing.ts:152-166).
IMPROVE
- A-I1 — Approval binds at approval time, not at billing time.
EXTEND· SILENT (over-billing → dispute)approveOveragecorrectly expires if hours changed before the facility clicked — but nothing re-checks after. Once a week has any approved row, the cap is lifted for the current actual hours (generate.ts:306-307) and the variance flag is suppressed (lib/invoice/index.ts:244). Facility approves 45h; 3 more hours get logged late; the invoice bills 48h uncapped, unflagged. The staleness guard forces a re-Correct, but Correct recomputes with the same lifted cap. Minimal fix: cap the lifted line atapproval_requests.hours_worked(billmin(actual, approvedSnapshot)when approved), or re-raise the variance flag whenactual > snapshot + ε. - A-I2 —
acknowledgeUnapprovedis batch-wide, but reads per-invoice.EXTEND· SILENT (narrow) The dialog computes one flag:chosen.some(d => d.unapprovedHours > 0)(ReviewAndSendDialog.tsx:105-112), and the RPC applies it to every id (0097:95-99). Re-checking one flagged draft acknowledges unapproved hours on every selected draft — including hours that became unapproved after the dialog snapshotted. Minimal fix: pass the acknowledged invoice ids to the RPC instead of a boolean. - A-I3 — Hours entry is silent last-write-wins between actors.
EXISTS (accepted)· CAUGHT-ishcreateManualTimeEntriesupserts by (contractor, assignment, date); two people entering different corrections for the same day silently overwrite each other (LogWeekHoursForm.tsx:23-30). Acceptable for a one-operator shop; revisit only if a second biller ever exists. - A-I4 — Variance-flag operator drift between grid and shelf.
EXISTS· COSMETIC The run RPC flags at>=threshold and ignores per-line approved overage (0088:119-121); the lib flags strictly>with approval suppression (lib/invoice/index.ts:244). At exactly-threshold, or on an approved-overage week, the grid shows ⚠ where the shelf shows none. Advisory-only either way; align the SQL to the lib when next touching the RPC.
REDO
- A-R1 — A mid-week assignment change silently drops or re-prices hours.
EXTEND· SILENT (under- or mis-billing)pickLatestPerContractorchooses exactly ONE assignment per contractor per week (generate.ts:125-141); entries are then summed only for the chosen assignment ids (generate.ts:263-267; mirrored in weeklyRun.ts:110-126 and the RPC'sdistinct onat 0088:35-48). So the standard way to change a rate mid-week — end the old assignment row Tuesday, new row effective Wednesday — makes Mon–Tue's hours vanish from the invoice: no line, no gap flag (hours were logged), no variance (the chosen assignment's own numbers look consistent). Payroll may still pay those hours. The mirror failure: editing the rate on the same row applies the new rate retroactively to the whole week — no proration, whole-period restatement. Why a patch won't hold: one-assignment-per-contractor-week is a structural assumption repeated in three mirrored places (RPC, preview, shelf) — patching one desynchronizes the live totals from the stored draft and trips the staleness guard forever. Correct approach: sum and price per assignment (one invoice line per in-window assignment, each capped by its own pro-rata contracted hours), keeping pick-latest only as a display collapse. Known sibling issue: #102 (two-facility contractors resolve to one arbitrary assignment).
Part B — Coverage-gap edge cases
KEEP
- B-K1 — Detection is live, never cached.
EXISTSweekly_billing_runrecomputes on every page load (weeklyRun.ts:43-53); a late-entered timesheet clears the gap on next render, and the send-time staleness guard covers the invoice side. A stale cached gap being billed/excluded is structurally impossible. Keep it derived. - B-K2 — Deliberate non-coverage has a first-class home.
EXISTSThe dated billing pause suppresses billing and gap noise, auto-resumes, and shows as its own status (0088:44-47, 126-130). - B-K3 — Period boundaries can't double-count.
EXISTSEntries are whole-day (entry_date, unique per assignment/date, ≤24h — 0002:221-236); Mon–Sun windows are disjoint, so a week's hours belong to exactly one run. Legacy Sun–Sat invoices overlapping a Mon–Sun week are handled by 0087's overlap-aware dedup everywhere (run, shelf, preview, create-RPC). A midnight-spanning shift lands on whichever date the contractor logged — there is no time-of-day to split; that's the accepted model, not a bug.
IMPROVE
- B-I1 — Data-entry error vs. real gap: not distinguished, and the wrong-facility case bills the wrong facility.
NET-NEW· SILENT Every gap is the same boolean (logged = 0 ∧ no approved leave, 0088:74-82). The nastier half: hours logged against the wrong assignment of a two-facility contractor bill the wrong facility (each side looks locally plausible — one facility gaps, the other bills within its cap). Nothing flags "same contractor, hours at two facilities on the same day" or "gap here + unusual hours there". Minimal fix: a same-day multi-facility anomaly feedingreview(the unusedtime_entries.anomaly_flaggedcolumn, 0002:233, is a ready-made hook). - B-I2 — A gap (or 'none') week that scrolls past never resurfaces.
NET-NEW· SILENT (uncollected) — shared with Part C The cockpit is strictly per-week and anchors to the latest closed week (page.tsx:123-135, PeriodBar.tsx:20-31). A gap left unresolved in week N is invisible in week N+1; no query anywhere lists "weeks with logged payable hours and no non-void invoice". This is the second-biggest quiet-money hole in the system (after AR). Minimal fix: an "Unbilled weeks" strip on the cockpit — weeks in the last N where any facility had payable hours and no invoice — one SQL view, one alert row. - B-I3 — Alert fatigue on structurally idle assignments.
EXTEND· CAUGHT (but trains rubber-stamping) An assignment a facility keeps open but doesn't use every week re-flags a gap every single run, forever; the only outs are pausing the whole facility or ending the assignment. With a red banner weekly (InvoicingCockpit.tsx:221-230), the operator learns to skim past gaps — which is exactly how a real one slips through. Minimal fix: per-week "dismiss gap with reason" (audited), or a per-assignment "not expected weekly" flag that downgrades its gap to informational.
REDO
-
B-R1 — Any leave overlap silences the whole week's gap.
EXTEND· SILENT (uncollected) The gap predicate checks that an approved leave overlaps the week at all, not that it covers the gap window:lr.start_date <= p_period_end AND lr.end_date >= p_period_start(0088:76-82). A one-day approved leave suppresses a five-day gap; the row then lands in 'none' / "No activity" (logged=0 → thelogged_hours = 0branch at 0088:134) with a neutral badge and $0 billed — nobody is asked to review anything. Four worked-but-unlogged days are never billed and never chased. Same window logic in the facility shelf'sleavestatus (weeklyRun.ts:625-641). Why a patch won't hold: the boolean has no notion of which days leave covers, and both sides are day-granular already. Correct approach: day-level comparison in the RPC — gap unless (leave days ∪ logged days) covers the expected working days; anything partial staysgap. -
B-N1 (note) — The gap block is UI-only:
generateDraftInvoicenever checkscoverage_gap(invoicing.ts:135-176); the UI hides generate for gap rows (InvoicingCockpit.tsx:33-39). A direct server-action call bills the partial week (gapped contractor bills $0). Low risk (owner-gated), but the server should own the rule if it's a rule.
Part C — End-to-end process
Prod snapshot (2026-08-14): 0 open drafts · 1 check awaiting review · 4 stale unapproved
entries (1 contractor, oldest 2026-07-20) · cron.job is EMPTY — none of the five documented
scheduled jobs has ever been wired (docs/cron-setup.md) · $75.0k
outstanding, $53.7k past due.
| # | Stage | Current state | SPOF? | Recommended forcing function |
|---|---|---|---|---|
| 1 | Time capture (Hubstaff pull, manual entry, shelf Log-hours) | Manual-only — imports and entries happen when a human triggers them; no "hours missing for week N" signal beyond the gap flag | Y — contractor + owner | The unbilled-weeks strip (B-I2) doubles as the missing-hours alarm |
| 2 | Approval | Manual-only — owner-only bulk approve, no cadence; unapproved hours still bill (PAYABLE includes submitted) behind the soft gate, so nothing ages out of billing — but nothing forces the approval either | Y — owner | Weekly nudge: "Nh unapproved older than 14 days" on the cockpit; currently only visible per-week |
| 3 | Draft generation | Manual-only, no process for the skipped week — if nobody opens the tab on Monday, billing simply doesn't happen and the week never resurfaces (B-I2). Sound when exercised | Y — owner; illness/vacation = billing halts silently | Either schedule draft generation (cron → generateDraftInvoice per ready facility) or, minimally, the unbilled-weeks alert. Also: cross-train one staff member; today every mutating step is one person |
| 4 | Review → send | Sound — the strongest stage: recipient + due-date + amount-anomaly shown per row, staleness hard-guard, held/skipped/stale buckets reported, not swallowed (send.ts, ReviewAndSendDialog.tsx) | Y — owner clicks it | None needed beyond A-I2 (per-invoice acknowledge) |
| 5 | Delivery | Exists, watched by nobody — outcome recorded (email_status, outbox), failures badge on the list (page.tsx:79-104), resend exists; but a failed delivery waits for a human to browse the list (0 failed today) | N (recorded) | Surface email_status='failed' count on the cockpit KPI row, not just the All-invoices tab |
| 6 | Payment capture (checks) | Sound tooling, manual trigger — OCR + ref-first targeting + balance-moved guard + unapply are genuinely strong; but an unreviewed check has no ageing alarm | Y — whoever opens the mail | "Check awaiting review for >7 days" nudge |
| 7 | AR chase (due date → cash) | NO PROCESS — pure hope. See Part D | Y — owner's memory, with financial consequence | Implement the AR page (docs/design/accounts-receivable-page.md) |
| 8 | Interest / late fees | NO PROCESS in effect — engine exists, nightly job exists in code, cron never scheduled, enabled on 1/581 facilities, and accrued interest is display-only (never added to any invoice total/PDF/email) | Y | Wire cron (5-minute SQL); decide whether interest is policy or delete the columns |
| 9 | Record-keeping | Sound — audit_log on send/void/correct, outbox, invoiced-hours audit (0098), payout ledger (0107) | N | — |
| 10 | Scheduler itself | NO PROCESS — nothing detects that cron.job is empty; every "nightly" behavior has silently never run since go-live | Y — deploy-time human step | An in-app "last nightly run" heartbeat card (the jobs already hit app routes; record a timestamp and warn when stale) |
Single point of failure, named: every mutating step (approve, generate, send, reconcile,
chase) is the owner. There is no handoff document — the brief's INVOICING-HANDOFF.md doesn't
exist, which is itself a finding. Minimum viable handoff = one page: the Monday loop (stage 3–4
clicks), the check-import loop, and the "what the statuses mean" table from
status.ts.
Part D — AR reconciliation & overdue reminders
Classification per piece:
| Piece | Verdict | Evidence |
|---|---|---|
| Detection (overdue flip) | EXTEND | Correctly derived on read, never stored: lib/invoice/status.ts:24-39, invoice_ar_summary (0089:23-38), KPI card, dashboard ?filter=overdue link. Design is sound (no cron dependency, no stale stored state — prod confirms status='overdue' has never been written). But detection is pull-only: it exists exactly as long as someone opens the page. No push, no digest, no task. |
| Reconciliation (payment → invoice) | EXISTS | The strongest subsystem reviewed: fingerprint-deduped packet import, ref-first remittance targeting with ambiguity holds (lib/reconcile/target.ts), reviewed-allocation binding + balance-moved guard (0099) at confirm (checks.ts:488-621), whole-check unapply (0105). Partial → partial; unmatched payer → needs_review + facility pick. One soft edge: a multi-facility payer's held-back leftover isn't persisted as first-class "unapplied cash" — it's implied by amount_cents − Σapplications, visible only if someone inspects the check. The AR design's UNAPPLIED tile is the fix. |
| Reminder cadence | NET-NEW | facilities.reminder_schedule, email_template_reminder, email_template_overdue have zero consumers (grep: only db/types.ts + migration 0002) and reminder_schedule is set on 0 of 581 facilities. Pure aspiration — schema without a process. |
| Interest & escalation | EXTEND (interest) / NET-NEW (escalation) | Interest engine + nightly recompute + manual per-invoice recompute all exist (lib/interest, cron.ts:34-111, invoices.ts:152-214) — but (a) the cron was never scheduled (prod cron.job empty), (b) it's enabled on 1/581 facilities, and (c) accrued interest is display-only: it never joins the invoice total, PDF, or email, so even a computed figure is never billed or collected. Escalation beyond email: nothing — no ladder, no hold-service, no write-off (debt cannot exit the queue except by payment). All of this is designed, in detail, in docs/design/accounts-receivable-page.md — unimplemented, on this branch. |
| Visibility | EXTEND | What exists: the "Outstanding AR" KPI card (lump outstanding + overdue, KpiRow.tsx:19-29) and the ?filter=overdue invoice list. What doesn't, anywhere in the app: aging buckets (31–60/61–90/90+), per-facility/payer AR rollup, ranked chase queue, last-contact memory. Not elsewhere either — this is a real gap, and the AR page design is its specified fix. |
The actual test (real data, 2026-08-14)
Westchester Center for Rehabilitation & Nursing: 22 open invoices, $52,801.40 outstanding,
$35,721.40 past due, oldest due 2026-04-15 — 121 days. Trace from due_date passing to cash:
- Due date passes → nothing happens. Stored status stays
sent; "overdue" exists only as a derived label when a page renders it. - Anyone notices only if the owner opens
/admin/invoicesand reads the KPI card or the red pill — human memory, nudge #0 never fires. - No reminder email is ever sent (no consumer of
reminder_schedule/templates) — human memory. - No interest accrues (cron unscheduled; facility not enabled; and it wouldn't bill anyway) — policy exists on paper only.
- There is no record of "I called them July 28, they promised payment" anywhere in the system — follow-up cadence lives in the owner's head — human memory.
- No escalation rung, no hold-service, no write-off — the invoice sits in
sentforever or until a check happens to arrive. Four sibling facilities are in the same state ($6.8k / $6.1k / $4.2k / $0.9k past due).
Every single step depends on one person remembering, with zero system nudges. This is not a hypothetical silent failure — it is currently occurring, measured at ~$53.7k.
Part E — UX/UI pass (shipped screens: cockpit, two shelves, Review & send dialog)
KEEP
- E-K1 — One component system, status-routed shelves. The brief worried about four
stitched-together screens; the shipped UI is one kit (shared
SlideOver,Badgetones,Table, status metadata in status.ts) and the row click routes by status to the shelf that usually matters (InvoicingCockpit.tsx:212-215). Consistency is not the problem here. - E-K2 — The common weekly path is already short and safely shaped. Clean week: select-all → "Create invoice for selected" → "Review & send" → Send ≈ 5 clicks for the whole book, with the ceremony concentrated at the one irreversible step, drafts deliberately cheap/reversible (InvoicingCockpit.tsx:267-284). The PeriodBar refusing to advance past the last closed week is quiet correctness — keep it (PeriodBar.tsx:17-31).
- E-K3 — The send confirm confirms something real. Per-draft recipients, due date, and an amount-anomaly check against the facility's recent baseline (ReviewAndSendDialog.tsx:74-89, weeklyRun.ts:770-839) — designed for the tired reviewer, not the attentive one. This is the right pattern; extend it rather than adding more generic "are you sure" steps.
IMPROVE
- E-I1 — Gap rows open the one shelf that can't fix a gap. (actively causes mistakes)
openShelfroutesgapto the Facility shelf (InvoicingCockpit.tsx:212-215), which shows "No activity" badges but has no remedy; the Log-hours form lives on the Invoice shelf (InvoiceShelf.tsx:175-199), and a gap row's invoice cell renders "—" (no link, InvoicingCockpit.tsx:338-357). The status whose whole point is "someone should enter/verify hours" has no path to the hours form. Fix: routegapto the invoice shelf, or mountLogWeekHoursFormon the facility shelf's gap state. - E-I2 — "Click Correct" points at a control on a different tab. (actively slows the recovery loop)
The stale-send result and the shelf's out-of-date note both instruct "click Correct"
(ReviewAndSendDialog.tsx:242-259,
InvoiceShelf.tsx:263-267) — but
Correct exists only in the All-invoices tab's row actions
(InvoiceActions.tsx:213-233). The most
common exception loop (hours moved → correct → resend) forces a tab switch and a row hunt.
Fix: a Correct button in the invoice-shelf footer for drafts (calls
recomputeInvoice). - E-I3 — Under-contract confirm: two paths, two behaviors. (inconsistent, one is a dead end)
Batch path:
confirmDanger→ confirm → create (InvoicingCockpit.tsx:186-201). Shelf path:generate()ignoresneedsConfirmand dead-ends on the raw error (InvoiceShelf.tsx:67-78 vs invoicing.ts:168-171). Same judgment, different outcome depending on which button you used. - E-I4 — 'None / No activity' is an overloaded bucket hiding the B-R1 leak. (actively hides mistakes)
'none' mixes "no assignments — genuinely nothing to bill" with "assigned people, zero logged
hours, gap suppressed by a partial leave" — the exact case where money silently goes unbilled
— under one neutral badge (0088:134,
status.ts:11). Five statuses is the right
count; this one has the wrong contents. Fix: split the label ("Nothing to bill" vs
"No hours logged"), and fix B-R1 so the partial-leave case stays
gap. - E-I5 — Per-row "send anyway" checkbox, batch-wide contract. (cosmetic wording, real semantics — see A-I2) The row copy reads "check to send anyway" as if the acknowledgment were per-invoice (ReviewAndSendDialog.tsx:344-349); the wire contract is one boolean for the batch. Fix with A-I2.
REDO
- E-R1 — AR has no home; the cockpit's per-week lens can't be stretched to cover it. The one AR surface is a lump-sum KPI card + a flat overdue filter list. Aging buckets, payer grouping, chase ranking, contact memory, escalation — none has a place in the weekly-run mental model, and bolting them onto the cockpit would break its (good) one-week frame. This is a new page, and it is already fully designed: docs/design/accounts-receivable-page.md. Build that rather than extending the cockpit.
Ranked: what to fix first
- AR chase is NO PROCESS and is already losing money — implement the AR page (Part D, E-R1).
Not hypothetical: $53.7k currently past due with zero system nudges; the worst facility is
121 days out. Everything from detection-push to follow-up memory to write-off is missing, and
the design (+ its one
collection_activitiesmigration) is already written on this branch. Even a v1 with just the Chase queue + activity log removes the "owner's memory" dependency from every step of the Part D trace. (Compliance: invoicing migration → security-reviewer + human approval; apply via MCP.) - Wire the cron jobs —
cron.jobis empty in prod (Part C stage 10). Five minutes of SQL from docs/cron-setup.md turns on everything "nightly" that has silently never run (interest accrual, Wise reconcile, pipeline refresh). Add the heartbeat card so this class of failure can't be silent again. Highest value-per-minute item in this report. - Leave-window gap suppression (B-R1) + the 'none' bucket relabel (E-I4).
A one-day approved leave hides a five-day gap in a neutral tab — quiet non-billing that
matches Nightingale's actual operating pattern (PH contractors, approved leaves, 9+
facilities weekly). One day-level predicate change in
weekly_billing_run+ a mirrored shelf check closes it.
Close behind: A-R1 (mid-week assignment swap drops hours — strike whenever a rate change or renewal is effective-dated mid-week) and B-I2 (the unbilled-weeks ledger — cheap insurance that also covers the "owner on vacation" SPOF in Part C stage 3).