Design: Accounts Receivable (AR) Management page
Route: /admin/receivables · Access: requireAccess('billing') (page) + re-verified in every server action (CLAUDE.md §Security guardrails).
Status: design only — no code. All example data below is invented.
Compliance note: this document was produced without reading any server code, schema, route handlers, or data. Everything about existing persistence is taken from
docs/reconciliation/report-scope.md; where that document is silent, the assumption is stated inline and listed in §11.
1. The recovery workflow this page serves
The owner opens this page with one question: "who owes us money, why haven't they paid, and what do I do about it right now?" The page answers it by splitting all un-collected dollars into exactly two kinds of work, because they need different people-skills, different tools, and different urgency:
Work type A — "Fix our books" (internal, zero facility contact). DEAD + UNAPPLIED dollars, plus every MISSING invoice whose cause chip is an us-problem: email failed (#2), payer unmapped (#3), check unreadable (#4), pre-ref-first allocation (#5), superseded (#6), voided/deleted with payments (#7/#8), overpayment swallowed (#9). This money is recoverable by an admin at a desk in minutes. It is the highest-ROI work on the page and it comes first, for a second reason that is a hard rule of the design:
Never dun a facility for money we already hold. An invoice with an active internal cause chip is excluded from its facility's chaseable balance and its dunning actions are disabled until the chip is cleared. Calling Maplewood Care about $4,200 that is sitting in our own unapplied-check pile destroys the credibility every other collection call needs.
Work type B — "Chase the facility" (external, relationship work). MISSING dollars with no internal excuse: the invoice was sent, delivered, is past due, and the money simply hasn't arrived. This is phone-and-email work, done per relationship — and because sibling facilities often share one payer/management company, the queue groups by payer group (facility when no group exists), not by invoice. One call to Beacon Health Management covers Maplewood Care and Cedar Grove Rehab in a single conversation; the UI must present it that way (guidelines §Match Between System & the Real World — mirror how the task is actually done).
Drafts (cause #1, "never sent") are not owed yet and are not collections work. They get one quiet line under the tiles linking to the invoicing cockpit, which owns sending. Anything "never generated" is explicitly out of scope (report-scope §3.1 — cockpit owns it).
Aging × cause
Standard aging buckets (current / 1–30 / 31–60 / 61–90 / 90+, off due_date) are computed
for every open invoice, but aging ranks only work type B. In work type A, aging is a
display column, not a priority signal — a 90-days-overdue invoice whose email bounced is not
"a hard collection case", it is a 30-second resend that has been losing us interest for
90 days. The cause chip decides which queue; aging decides where in queue B.
2. Prioritization model
Two rules, both explainable in one sentence on the phone (guidelines §Trust — staff will not act on a number they cannot defend):
- Follow-ups due come first. Any payer group whose
next_followup_on ≤ today(including broken promises — a promise logs its expected date as the follow-up) pins to the top of the chase queue, ordered by follow-up date ascending. Commitments beat math. - Then dollar-days. Everything else ranks by
Σ over chaseable invoices (outstanding_cents × days_past_due), descending. $10,000 overdue 3 days ranks with $1,000 overdue 30 days. One number, no tuning knobs, monotone in both the things that matter. The score itself is shown on hover of the rank position, never as a mystery ordering.
Within work type A there is no scoring: it is a short list (tens of rows, not hundreds), sorted by dollars descending. If it ever isn't short, that is an operational fire, not a sorting problem.
Rejected alternatives: bucket-weighted scores (arbitrary weights = indefensible on the phone); ML/heuristic "likelihood to pay" (no training data, no trust); pure aging-bucket grouping (lets a $50 invoice at 91 days outrank $12,000 at 60 days).
3. Boundary with the per-facility reconciliation report
docs/reconciliation/report-scope.md specifies the diagnostic: one facility, full
evidence, per-row provenance expansion, dead-invoice ledger, payments panel, CSV, the
(deferred) tokenized facility statement. This page is the collections cockpit: company-wide,
ranked, action-first, with workflow memory. The split:
| Concern | AR page (this doc) | Reconciliation report |
|---|---|---|
| Scope | company-wide, all open money | one facility, date range |
| Unit | payer group / facility, ranked | invoice + payment rows, exhaustive |
| Purpose | decide + act + record | verify + explain + export |
| Provenance depth | tile/row summary via Money source/asOf | full <details> expansion to check numbers, confirmers, OCR confidence |
| Dead/unapplied money | totals + one-line items + links to resolve | the ledger of record (B2/B4 both sides) |
| Workflow (contacts, promises, stage) | owns it | none |
| CSV export | none (cut — recon owns export) | owns it |
Cross-links: every facility row and every SlideOver header here links to
/admin/facilities/[id]/reconciliation ("Full evidence →"); the reconciliation report header
gains one link back ("Collections view →" → /admin/receivables?q=<facility>). Unapplied
check rows link to /admin/checks (application UI lives there — this page never rebuilds it)
and payer-mapping rows to /admin/checks/payers. Nothing is duplicated: this page shows
that $500 is stuck on check #4417 and where to fix it; the recon report shows why, line
by line.
Tile definitions are exactly report-scope §1, computed company-wide over all time (not a
date range — deviation from the recon report, deliberate: MISSING/DEAD/UNAPPLIED are
point-in-time-open numbers, and computing BILLED/RECEIVED over the same unbounded window is
what makes the identity BILLED = RECEIVED_on_live + MISSING hold exactly on screen. A
range picker would break the displayed identity and this page's numbers must never need a
footnote). The recon report keeps its range picker for period analysis.
4. Layout
Desktop (≥1024px)
┌ Admin › Receivables ────────────────────────────────────────────────────────────────┐
│ Accounts receivable Data as of 2026-08-07 09:14 │
│ Get owed money collected. Open invoices are always included. │
├──────────────────────────────────────────────────────────────────────────────────────┤
│ ┌ BILLED ─────┐ ┌ RECEIVED ───┐ ┌ MISSING ────────────┐ ┌ DEAD ───┐ ┌ UNAPPLIED┐ │
│ │ $412,800 │ − │ $371,300 │ = │ $41,500 │ │ $6,900 │ │ $3,200 │ │
│ │ all time │ │ on live inv.│ │ ▲ recovery target │ │ ⚠ leak │ │ ⚠ leak │ │
│ └─────────────┘ └─────────────┘ └─(links to Chase)────┘ └─(→Fix)──┘ └─(→Fix)───┘ │
│ Drafts not yet sent: $12,400 (3 invoices) → Invoicing cockpit │
├─ Fix our books first — $12,340 recoverable without calling anyone ──────────────────┤
│ What Facility Amount Age Action │
│ ⚠ Email failed INV-1207 Harborview SNF $4,200 41d [Resend invoice] │
│ ⚠ Unapplied leftover chk#4417 Beacon Health… $500 12d Apply → /admin/checks│
│ ⚠ Payer unmapped chk#4423 (unknown payer) $2,140 6d Map → /checks/payers │
│ ⚠ Superseded INV-1180→1191 Maplewood Care $3,800 77d Review → recon report│
│ ⚠ Verify allocation INV-1163 Cedar Grove Rehab $1,700 98d Review → recon report│
│ 5 of 5 · sorted by $ desc │
├─ Chase — $29,160 owed with no internal excuse ──────────────────────────────────────┤
│ [Search payer/facility…] [Aging ▾] [Stage ▾] │
│ Payer / facility Owed Aging (worst) Last contact Next step Stage │
│ ▸ Beacon Health Mgmt $14,700 $8,400 in 90+ Jul 28 · call DUE TODAY Phone ⋯ │
│ covers Maplewood Care + Cedar Grove Rehab · promise broken Jul 31 │
│ ▾ Harborview SNF $9,260 $5,060 in 61–90 Jul 15 · email Aug 12 Statement⋯│
│ INV-1198 Jun 1–7 due Jun 21 $5,060 outstanding 61–90 │
│ INV-1211 Jul 6–12 due Jul 26 $4,200 outstanding 1–30 │
│ blocked from dunning: INV-1207 $4,200 [email failed → Fix queue] │
│ ▸ Summit Ridge Nursing $5,200 $5,200 in 31–60 — never not scheduled Reminder⋯│
│ 12 payer groups · follow-ups due first │
└──────────────────────────────────────────────────────────────────────────────────────┘
Hierarchy rationale (guidelines §Visual Hierarchy): MISSING is the one number that is impossible to miss — largest type in the tile row, and the tile row reads as the identity equation so the recovery target is visibly derived from billed-minus-received, not asserted. The Fix section sits above Chase because it is faster ROI and because its items gate dunning; it is compact (a short table) so the Chase queue — where staff spend their time — still owns the fold. F-pattern: scan tiles, drop into first table row, act (guidelines §Visual Hierarchy, "F-pattern for data-heavy screens").
Clicking MISSING scrolls to Chase; DEAD/UNAPPLIED scroll to Fix with the matching rows filtered. BILLED/RECEIVED are non-interactive context.
Row click (or Enter) on a Chase payer row opens the collection panel (SlideOver):
┌─ SlideOver ── PAYER GROUP ──────────────────────────────┐
│ Beacon Health Management [×] │
│ covers Maplewood Care · Cedar Grove Rehab │
├─────────────────────────────────────────────────────────┤
│ Owed $14,700 · oldest due May 9 · stage: Phone │
│ Full evidence → Maplewood recon · Cedar Grove recon │
│ │
│ OPEN INVOICES (chaseable) │
│ INV-1176 Maplewood due May 9 $8,400 90+ │
│ INV-1189 Maplewood due Jun 13 $2,500 31–60 │
│ INV-1195 Cedar Gr. due Jun 27 $3,800 31–60 │
│ BLOCKED (fix first): none │
│ │
│ ACTIVITY │
│ Jul 28 Call — spoke w/ AP dept, promised $8,400 │
│ by Jul 31 · follow-up Aug 1 — O.T. │
│ Jul 10 Reminder email sent (INV-1176) — O.T. │
│ Jun 30 Statement sent — O.T. │
│ │
│ LOG ACTIVITY (single column, labels above fields) │
│ Type (required) [Call ▾] │
│ Notes (required) [________________________] │
│ Promised amount (optional) [$______] │
│ Promised by (optional) [date input] │
│ Next follow-up (required) [date input] │
├─────────────────────────────────────────────────────────┤
│ [Escalate ▾] [Log activity] │
└─────────────────────────────────────────────────────────┘
Mobile (<640px)
Tiles stack single-column, MISSING first (it is the page's answer; BILLED/RECEIVED demote to
a two-up context row below it). Both tables use Table with default stackAt='sm' — every
Td carries a label, so each row renders as a named-field card (guidelines §Data
Visualization & Tables: never shrink, stack). The report-scope explicitly calls out that this
codebase's invoices page opted out with stackAt="none"; this page must not.
┌──────────────────────────┐
│ Accounts receivable │
│ Data as of Aug 7, 09:14 │
├──────────────────────────┤
│ MISSING $41,500 │
│ ▲ recovery target │
├──────────────────────────┤
│ Billed $412.8k·Rcvd 371.3│
│ Dead $6.9k · Unapp $3.2k │
├─ Fix our books ($12,340)─┤
│ ┌──────────────────────┐ │
│ │ ⚠ Email failed │ │
│ │ Invoice INV-1207 │ │
│ │ Facility Harborview │ │
│ │ Amount $4,200 │ │
│ │ [Resend invoice] │ │
│ └──────────────────────┘ │
│ … │
├─ Chase ($29,160) ────────┤
│ ┌──────────────────────┐ │
│ │ Beacon Health Mgmt │ │
│ │ Owed $14,700 │ │
│ │ Worst $8,400 90+ │ │
│ │ Next step DUE TODAY │ │
│ │ Stage ● Phone │ │
│ │ [Open panel] │ │
│ └──────────────────────┘ │
│ … │
└──────────────────────────┘
The SlideOver already renders full-height at w-[calc(100%-2rem)] on phones; no extra work.
5. Components and states
Reused primitives (from src/components/ui/), with the guideline section each satisfies:
| Component | Use here | States to implement | Guideline |
|---|---|---|---|
PageHeader | title, crumb Admin › Receivables, "Data as of" in description | n/a (static) | §Navigation; §Trust (provenance) |
Money | every amount, with source (e.g. "invoices − applications", "check #4417 leftover") and asOf | hover title always; caption on tiles | §Trust — "show source + last-updated on money values" (CLAUDE.md trust signals) |
Badge (+ dot) | cause chips, stage pill, aging-bucket label | tone per table below; dot ensures never-color-alone | §Color — semantic only, paired with text |
Tooltip | cause-chip hover: one sentence of meaning + the action ("Email to billing@harborview… failed Jun 27 — resend re-delivers the original invoice") | hover + focus visible | §Recognition over Recall — reveal taxonomy on hover |
Table/Th/Td | both queues; numeric on money columns; label on every Td | loading (skeleton rows), empty (EmptyState), error (Alert), mobile stacked | §Tables; §Feedback |
SortableTh | Chase columns (Owed, Last contact, Next step) — sorting overrides the default rank, a "Reset to priority order" link appears | aria-sort, focus ring (built in) | §Flexibility & Efficiency |
FilterBox | Chase: search payer/facility + Aging + Stage selects; URL-driven so views are shareable | debounced, back-button-safe (built in) | §Navigation — search when content exceeds a scan |
SlideOver | collection panel | native <dialog> focus trap, Esc, backdrop (built in); body scroll; pinned footer | §A11y; §Progressive Disclosure |
RowActions (⋯) | per row: Open panel / Log activity / Send reminder / Full evidence → recon / Escalate | keyboard model built in; disabled for gated items | §Affordances |
Field/Input/Select/Textarea | log-activity form | inline validation on blur, error text beside field, input preserved on error, submit disabled in flight | §Forms — all bullets |
DangerConfirm (confirmDanger) | "Hold service" escalation only — with consequence: "Contractors will stop being scheduled at Maplewood Care and Cedar Grove Rehab until the hold is lifted." and confirmWord: 'HOLD' | built in | §Error Prevention — confirm + describe consequence |
Alert | per-section degraded reads; identity-violation banner (see §8) | tones info/warning/danger | §Error recovery |
EmptyState | each queue when empty | offers the first action | §Error Prevention — helpful empty states |
Toast (notify) | success confirmation for every action ("Reminder sent to Harborview SNF", "Activity logged — next follow-up Aug 12") | success/error tones | §Feedback — confirm success explicitly |
Skeleton/Spinner | loading.tsx tiles + table rows | reserve space, no layout shift | §Speed as an aesthetic |
Button | primary = the queue-row action; ghost = secondary | full state set (exists) | §Affordances |
Cause chip mapping (tone + text + icon-dot; never color alone):
| Cause | Chip text | Tone | Primary action |
|---|---|---|---|
| #2 email failed | email failed | danger | Resend invoice |
| #3 payer unmapped | payer unmapped | warning | Map payer → /admin/checks/payers |
| #4 check unreadable | check needs review | warning | Review → /admin/checks |
| #5 pre-ref-first | verify allocation | warning | Review → recon report |
| #6 superseded | superseded by INV-1191 | info | Review → recon report |
| #7/#8 void/deleted w/ $ | void — had $3,800 paid | danger | Review → recon report |
| #9 overpayment swallowed | payer overpaid | warning | Apply leftover → /admin/checks |
New primitives — exactly one: StatTile (src/components/ui/StatTile.tsx): label +
Money child + optional badge + optional href/scroll target, with default / hover / focus
(when interactive) / loading-skeleton / zero-quiet states. Why nothing existing fits:
Card is an unopinionated surface; the tile needs consistent label/value/provenance
composition, an interactive variant with focus treatment, and a "zero renders neutral, never
red" rule — and the reconciliation report page (report-scope §4) needs the identical tile, so
one shared primitive prevents two divergent ad-hoc versions. Everything else on this page is
composition: the aging summary is plain text in a Td ("$8,400 in 90+" — a mini bar chart
was considered and cut: five labeled numbers beat unlabeled colored pixels, §Data-ink), the
cause chip is Badge+Tooltip, the activity timeline is a feature component
(src/components/admin/CollectionPanel.tsx), not a ui primitive.
6. Interaction flows
A. Chase an unpaid invoice. Tiles → Chase queue top row (Beacon Health Mgmt, DUE TODAY)
→ Enter/click opens SlideOver → staff sees chaseable invoices, the broken promise in the
activity log, and both recon links for evidence → calls the payer (off-system) → fills LOG
ACTIVITY (Type: Call; Notes; Promised $8,400 / by Aug 14; Next follow-up Aug 15 — the
follow-up field auto-fills from the promise date, editable) → [Log activity] → optimistic
row update (guidelines §Feedback): panel closes, toast "Activity logged — follow-up Aug 15",
the row drops out of DUE TODAY and re-ranks; rollback + inline error on failure with the
form input preserved.
B. Reclaim unapplied cash. Fix queue row "Unapplied leftover chk #4417 · $500" → action
is a link, not an in-page form: /admin/checks?check=4417 where the existing application
UI already lives. On return the row is gone (server recompute). This page routes; it never
reimplements cash application. Same pattern for payer-unmapped (→ /admin/checks/payers)
and every review-type cause (→ recon report). The Fix queue is a to-do list over existing
tools.
C. Resolve a bounced-email invoice. Fix row "Email failed INV-1207" → [Resend invoice]
(the one Fix action that is in-page, because it is one idempotent server action that
already exists for invoice sending) → button enters loading state, disabled → success toast
"INV-1207 resent to billing@harborview.example" → row leaves the Fix queue; INV-1207's
$4,200 moves from "blocked" into Harborview's chaseable balance in the same render. Failure:
inline Alert on the row with the SMTP error summary and a retry — never a dead end
(§Error recovery).
D. Escalate an aged debt. Chase row ⋯ → Escalate ▾ shows the ladder with the current
rung marked: Reminder → Statement → Phone → Formal demand → Hold service. Selecting
"Formal demand" opens the log-activity form pre-typed to Demand (staff records how it was
delivered; the page does not generate demand letters — see §11). Selecting Hold service
first runs confirmDanger (title "Hold service for Beacon Health Management?", message
naming the owed total, consequence naming the affected facilities, confirmWord: 'HOLD'),
then logs a hold activity. The stage pill is derived from the latest escalation-type
activity — no separate stage state to drift. De-escalation = logging a lower rung, which the
form allows with an "are you de-escalating?" inline note, not a blocker.
7. Data requirements
Exists today (per report-scope; no reads performed): everything needed to compute the
page — invoices (total_amount_cents, amount_paid_cents, status, due_date, sent_at,
email_status, deleted_at, replaces_invoice_id), check_payments (+ status lifecycle,
ocr_*), check_payment_applications, remittance-line match_basis (for cause #5). All
five tiles, aging, manual residual, leftover, and all nine cause chips are pure derivations —
the classification logic should live in a pure module (src/lib/ar/queue.ts, mirroring the
src/lib/reconcile/ledger.ts split in report-scope §6) so tests own every predicate, and it
must reuse the recon ledger's bucket predicates rather than re-deriving them (one source of
truth; the two pages must never disagree on a dollar).
New persistence — one table, one migration (this is the entire workflow-state design):
CREATE TABLE collection_activities (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
company_id uuid NOT NULL REFERENCES companies(id),
facility_id uuid NOT NULL REFERENCES facilities(id),
invoice_id uuid NULL REFERENCES invoices(id), -- null = relationship-level
type text NOT NULL CHECK (type IN
('note','call','reminder','statement','demand','hold','dispute')),
body text NOT NULL,
promised_cents bigint NULL CHECK (promised_cents > 0),
promised_on date NULL,
next_followup_on date NULL,
created_by uuid NOT NULL REFERENCES profiles(id),
created_at timestamptz NOT NULL DEFAULT now()
);
-- Append-only: INSERT + SELECT for billing staff; NO UPDATE, NO DELETE grants —
-- matching this repo's ledger-lockdown posture. Corrections are new rows
-- (type='note', "correcting: …"). RLS company-scoped.
Everything else is derived from it: last contact = latest row per facility; next follow-up =
max(next_followup_on); stage = type of the latest escalation-type row
(reminder/statement/demand/hold); broken promise = promised_on past with the invoice(s)
still outstanding. No status columns on facilities or invoices, no snooze table, no CRM
objects. Payer-group rollups aggregate member facilities' activities.
Compliance: this migration touches invoicing data in a payroll/PII app — per CLAUDE.md it
requires the security-reviewer subagent and human approval before merge, and it must be
applied via MCP apply_migration (project memory: no supabase/ dir; never db push).
Assumed, needs verification (not read): a payer→facilities mapping (billing_payers is
referenced by the checks/payers admin page in report-scope §3). If it reliably maps a payer
to its member facilities, the Chase queue groups by it; if not, the queue is facility-level
with a neutral shared payer badge on siblings, and grouping ships in a follow-up. The page
design is identical either way; only the grouping key changes.
8. Empty / loading / error / degraded states
- Loading:
loading.tsxwithStatTileskeletons + two table skeletons, exact final dimensions reserved (no layout shift; §Speed as an aesthetic; skeletons >300ms rule). - Empty, the good kind: MISSING = 0, both leak tiles 0 → the queues collapse to one
EmptyState: "Nothing outstanding — every sent invoice is paid, applied, and clean." with a link to the invoicing cockpit. Zero tiles render neutral/quiet, never red or green (report-scope §4 precedent; §Color). - Empty Fix queue (the common good case): quiet single line "Books are clean — no internal recovery items." — the section shrinks, Chase moves up.
- Empty Chase after filtering:
EmptyState"No payer matches these filters" + clear- filters action (never a blank table). - Per-section degraded reads: each of (tiles, fix queue, chase queue, activities) is an
independent read; one failure renders an inline
Alert("Couldn't load collection activity — amounts are unaffected. Retry") in that section only, never a dead page (report-scope §4 precedent). If activities fail, the Chase queue still renders ranked by dollar-days with the workflow columns showing "—" and a warning chip "workflow data unavailable" — money data never hostages workflow data, and vice versa. - Identity violation (
BILLED ≠ RECEIVED_on_live + MISSING, or any negative residual/ leftover): a page-topAlert tone="danger": "These numbers don't reconcile (off by $12.34) — a data issue is hiding money. Open the reconciliation report." Never silently render numbers that don't add up; the equation tiles make the check visible. - Action errors: inline at the point of action, input preserved, retry offered (§Forms, §Error recovery). No dead-end "contact your administrator" copy.
9. Accessibility (guidelines Part 3 + §Forms + §Feedback)
- Semantic structure: one
h1(PageHeader),h2per Section, real<table>s; queue expansion uses native<details>/<summary>where no JS is needed (recon precedent),aria-expandedon the payer-row toggle where it is. - Full keyboard path: tiles (when interactive) → filters → sortable headers (
SortableThshipsaria-sort) → rows (Enter opens panel) →RowActions(complete keyboard model built in) →SlideOver(native<dialog>focus trap, Esc, focus returns to the trigger row). Tab order matches visual order. - Never color alone: every Badge pairs tone with text (+ dot); DUE TODAY is text, not a red cell; aging severity is stated ("$8,400 in 90+"), not encoded as a hue.
- Contrast: existing Badge/Alert token pairs are the already-audited AA combinations; new
StatTiletext uses slate-900/slate-500 on white (passes 4.5:1); focus rings are the existingfocus-visible:ring-brand-400treatment. - Touch: 44×44 minimum on row-action triggers and mobile card actions.
- Motion: no entrance animations on menus (RowActions is already reduced-motion safe);
skeleton pulse and SlideOver transition respect
prefers-reduced-motion; optimistic row re-ranking uses a fade, disabled under reduced motion. - Forms: single column, visible labels above fields, required and optional marked
explicitly, correct input types (
date, numeric for money), inline validation, errors beside fields in plain language. - Live feedback: toasts use a polite live region; the "Data as of" stamp updates on revalidation.
10. Scope cuts and what the brief implies but doesn't say
Cut, with reasons:
- CSV export — the recon report owns export; a company-wide dump invites dunning from a spreadsheet with none of the never-dun guardrails.
- Charts (aging histograms, trend lines) — five numbers and two ranked tables answer the
owner's question; a trend chart is a reporting feature for
/admin/reports. - In-page cash application / payer mapping — exists at
/admin/checks*; rebuilding it here would fork a money-mutation path (report-scope built the same fence). - Reminder templates / scheduling / auto-dunning — automation before the manual workflow has proven itself is how facilities get robo-dunned for money we hold. Revisit after a quarter of real activity data.
- Date-range picker — breaks the displayed identity (§3).
- Pagination — bounded rows (payer groups, not invoices). Revisit past ~200 groups.
The brief missed:
- Debt has no exit besides payment. The ladder ends at "hold service", but some debt is uncollectible; without a terminal state, MISSING grows forever and the queue silts up with zombies that bury live work. Needs an owner-defined write-off mechanic (probably void with a reason code — which today would move the money to DEAD, polluting the leak bucket, so DEAD likely needs to distinguish "written off deliberately" from "erased accidentally").
- The "Statement" rung has no artifact. The tokenized facility statement is PR-4 of the recon scope, deferred behind owner + security sign-off. Until it ships, "statement" is a logged activity describing a manual email. The AR page makes that gap visible and more expensive weekly — it should pull PR-4 up the priority list.
- Payment arriving doesn't notify the collector. When a check confirms against an invoice with an open follow-up, the follow-up is stale. V1: the panel shows "received $8,400 on Aug 12 — after your last contact" inline. Auto-clearing follow-ups is an owner decision (§11).
11. Open questions for the owner
- Write-off: what marks debt uncollectible, who may do it (owner-only, per the existing payout/hire gates?), and should it be distinguishable from accidental DEAD money? (Blocks nothing at launch; blocks queue hygiene within a quarter.)
- Payer grouping: does
billing_payersreliably map payers to member facilities today (East Northport/Huntington Hills-style aliases included)? Determines the Chase queue's grouping key (§7). - Hold service authority: owner-only or any billing staff? The
confirmDangerdesign works for both; the server gate differs. - Auto-clear follow-ups on payment received, or leave them for manual review? (Auto is convenient; manual catches partial payments that shouldn't end the conversation.)
- Statement artifact priority: does this page's existence pull recon PR-4 (tokenized statement) forward, or does "statement = manual email" hold for now?
12. Decisions (owner grilling, 2026-08-21)
All five §11 questions are resolved; this section wins where it contradicts earlier sections.
- Write-off: per ADR-0053 — nullable
written_off_at/by/reasonflag columns oninvoices, owner-only, reversible; built in v1, same migration.invoice_ar_summaryandneeds_attention_counts(0089) are updated in that migration to exclude written-off invoices — the AR page and the weekly-run strip must never disagree on a dollar. Written-off stays in the check-allocation pool (ADR-0053 rationale). - Payer grouping: settled by schema fact —
billing_payers(0049) is an OCR text-pattern router (pattern → one facility or covers-all), not a payer-group entity. V1 Chase queue is facility-level with a neutral "shared payer" badge (§7 fallback); a grouping entity ships later if ever needed. - Hold service: record-only in v1, honest copy — the confirm dialog states that the hold is recorded and scheduling is not automatically blocked ("act on assignments separately"). No enforcement wiring. Gate: owner-only (matches pay-run/hire/payout precedent). Ordinary activity logging (call/reminder/statement/demand) is billing staff.
- Follow-ups: auto-clear on full payment only; partial payments keep the conversation open. Implemented as pure derivation, no write path: an open follow-up counts only while debt that predates the activity is still outstanding — so a facility that pays off and later re-offends is not pinned by a stale follow-up.
- Statement artifact: pulled forward, but not as the tokenized public route (recon
PR-4 stays deferred). V1 statement = generated PDF emailed to the facility AP contact
via the existing email service, with selected invoice PDFs attached; sending auto-logs a
statementactivity. External document: internal diagnostics (cause chips, OCR confidence, provenance) never appear in it. Interest shows only where a real accrued figure exists (wiring the interest cron is in scope as its own prod step).
Reality corrections to §3/§7 (the doc was written blind): src/lib/reconcile/ledger.ts
and the per-facility reconciliation report page do not exist yet. PR-1 therefore builds
the pure ledger module (report-scope §1–§3 predicates) as the first home of these
predicates; the future recon report reuses it, not vice versa. Until that page exists,
"Review → recon report" actions point at the facility page / /admin/checks instead.
Build order: three sequential PRs, each behind security-reviewer + owner approval — PR-1 ADR + migration + pure logic; PR-2 the page; PR-3 statement + send. Model routing: Fable designs/plans and reviews; Opus implements.