Skip to main content

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):

  1. 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.
  2. 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:

ConcernAR page (this doc)Reconciliation report
Scopecompany-wide, all open moneyone facility, date range
Unitpayer group / facility, rankedinvoice + payment rows, exhaustive
Purposedecide + act + recordverify + explain + export
Provenance depthtile/row summary via Money source/asOffull <details> expansion to check numbers, confirmers, OCR confidence
Dead/unapplied moneytotals + one-line items + links to resolvethe ledger of record (B2/B4 both sides)
Workflow (contacts, promises, stage)owns itnone
CSV exportnone (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:

ComponentUse hereStates to implementGuideline
PageHeadertitle, crumb Admin › Receivables, "Data as of" in descriptionn/a (static)§Navigation; §Trust (provenance)
Moneyevery amount, with source (e.g. "invoices − applications", "check #4417 leftover") and asOfhover 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 labeltone per table below; dot ensures never-color-alone§Color — semantic only, paired with text
Tooltipcause-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/Tdboth queues; numeric on money columns; label on every Tdloading (skeleton rows), empty (EmptyState), error (Alert), mobile stacked§Tables; §Feedback
SortableThChase columns (Owed, Last contact, Next step) — sorting overrides the default rank, a "Reset to priority order" link appearsaria-sort, focus ring (built in)§Flexibility & Efficiency
FilterBoxChase: search payer/facility + Aging + Stage selects; URL-driven so views are shareabledebounced, back-button-safe (built in)§Navigation — search when content exceeds a scan
SlideOvercollection panelnative <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 / Escalatekeyboard model built in; disabled for gated items§Affordances
Field/Input/Select/Textarealog-activity forminline 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
Alertper-section degraded reads; identity-violation banner (see §8)tones info/warning/danger§Error recovery
EmptyStateeach queue when emptyoffers 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/Spinnerloading.tsx tiles + table rowsreserve space, no layout shift§Speed as an aesthetic
Buttonprimary = the queue-row action; ghost = secondaryfull state set (exists)§Affordances

Cause chip mapping (tone + text + icon-dot; never color alone):

CauseChip textTonePrimary action
#2 email failedemail faileddangerResend invoice
#3 payer unmappedpayer unmappedwarningMap payer → /admin/checks/payers
#4 check unreadablecheck needs reviewwarningReview → /admin/checks
#5 pre-ref-firstverify allocationwarningReview → recon report
#6 supersededsuperseded by INV-1191infoReview → recon report
#7/#8 void/deleted w/ $void — had $3,800 paiddangerReview → recon report
#9 overpayment swallowedpayer overpaidwarningApply 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.tsx with StatTile skeletons + 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-top Alert 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), h2 per Section, real <table>s; queue expansion uses native <details>/<summary> where no JS is needed (recon precedent), aria-expanded on the payer-row toggle where it is.
  • Full keyboard path: tiles (when interactive) → filters → sortable headers (SortableTh ships aria-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 StatTile text uses slate-900/slate-500 on white (passes 4.5:1); focus rings are the existing focus-visible:ring-brand-400 treatment.
  • 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:

  1. 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").
  2. 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.
  3. 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

  1. 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.)
  2. Payer grouping: does billing_payers reliably map payers to member facilities today (East Northport/Huntington Hills-style aliases included)? Determines the Chase queue's grouping key (§7).
  3. Hold service authority: owner-only or any billing staff? The confirmDanger design works for both; the server gate differs.
  4. 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.)
  5. 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.

  1. Write-off: per ADR-0053 — nullable written_off_at/by/reason flag columns on invoices, owner-only, reversible; built in v1, same migration. invoice_ar_summary and needs_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).
  2. 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.
  3. 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.
  4. 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.
  5. 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 statement activity. 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.