Records → Coaching Invoices tab shows none after the coaching-schema migration
Symptom¶
The Records page Coaching Invoices tab renders an empty list, which the owner suspected was caused by a data migration (the same way the WOPC restructure broke the WOPC tab — see I-011).
Owner, 2026-06-24 (verbatim): "The Coaching Invoices tab are also not showing any coaching invoices most possibly due to a migration of data (similar situation to the WOPC), can you help me look into that as well?"
Root cause (proven against live Firestore, tebs-mel)¶
The coaching-schema migration (T-079) collapsed Sessions/{id}/invoice/{docId} into a single
invoice map field on the session doc. The Records endpoint
(pages/api/accounting/matchable-coaching-invoices.ts) reads that map and hard-requires
invoice.invoiceNumber — if (invoiceNumber) { … } (line ~143); a session whose invoice map has
no invoiceNumber is skipped entirely.
Live data: 15 sessions carry an invoice map (under Students/{abbr}/Sessions/*), but 0 of
15 have invoice.invoiceNumber (nor a top-level one). The maps DO carry studentAbbr,
sessionOrdinal, issuedAt, lessonRate, etc. — everything except the number. So every coaching
invoice is skipped → empty tab.
- Not RBAC. The endpoint gates on MEL access; the owner is
super_admin(passes). (There is a latent RBAC issue for non-admins — the user docs store access assubsidiaryAccesswhilecanAccessSubsidiaryreadssubsidiaryIds— but that doesn't affect the owner and is out of scope for this ticket.) - The rest of the app already compensates for a missing number by reconstructing it
(
MEL-{year}/{studentAbbr}-{ordinal}(PR)) — seelib/accounting/reports.server.ts:1685andlib/accounting/derivedJournals.ts:529. The Records coaching endpoint never got that fallback.
Why this is escalated to T-079, not a new task¶
T-079 (Coaching schema cleanup, owned by Accounting (Infrastructure) —
https://claude.ai/code/session_015P6KzVYsQCLgEmUjR9bMwM) is the migration that owns this shape.
T-079's own design table specifies invoice.invoiceNumber as a queryable field (it even specs a
composite index on invoice.invoiceNumber for the lookup-by-number paths), so the field being
absent on every migrated session is a gap in that migration's done-state — that agent's scope, and
the owner is coordinating the coaching/RBAC work with them.
Two clean fix options (for whoever picks it up):
1. Backfill invoice.invoiceNumber onto every session invoice map during the T-079 migration (the
design already expected it + the composite index needs it), OR
2. Give the Records endpoint the same reconstruct-when-absent fallback the reports/journals already
use.
Not fixed by Records (Infrastructure) to avoid colliding with the in-flight coaching/MEL
restructure (the session top level has itself changed — studentAbbr/sessionOrdinal are gone
from the top level, now only inside the invoice map — so the exact reconstruction source is a moving
target until T-079 settles).
Decision log¶
2026-06-24 — opened + escalated to T-079 (diagnosed, handed off)¶
- ✅ Attestation (Records (Infrastructure)): read
AGENTS.md; scope-scanned — the coaching migration is T-079's scope, so this ticket cross-links there rather than opening a parallel task. - Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
2026-06-24 — FIXED via backfill (Accounting (Infrastructure))¶
- ✅ Attestation (Accounting (Infrastructure)): read
AGENTS.md; appending to this ticket Records (Infrastructure) opened (not editing their prose) + flippingescalated → fixedsince the symptom is resolved. The owner chose the backfill (populate the data) over read-path adaptors. - Source (edit): Accounting (Infrastructure) · https://claude.ai/code/session_015P6KzVYsQCLgEmUjR9bMwM
- Root cause refined: the number was never persisted pre-migration — the writer
(
app/coaching/invoice/[sessionId]/client.tsx→generateRequestNumber,lib/paymentRequest/types.ts) derives it deterministically fromyear + studentAbbr + ordinal. The legacy invoice bodies the T-079 migration copied never had the field, so the map lacked it. (The current writer DOES persistinvoice.invoiceNumberfor new invoices — only the legacy 15 were stranded, so this was a one-time data gap, not a recurring bug.) - Fix — deterministic backfill (no adaptors, no deploy): recomputed each number with the exact
canonical formula
MEL-{year}/{abbr}-{ordinal:03d}(PR)and wrote it toinvoice.invoiceNumberon all 15 legacy sessions in prodtebs-mel. SurgicalupdateMask(only that subfield; rest of the invoice map preserved). 15/15 written, 0 failed; backup dumped first. - Correctness proof: the recomputed value matched the pre-existing
detail.invoice.number(from the T-077/T-078detail.*shape) byte-for-byte on the verified sample (MEL-2025/MT-011(PR)) — i.e. the backfill reproduced the real issued number, not a guess. - Script committed:
scripts/backfill-coaching-invoice-number.py(dry-run default,--apply, backup) — re-run is idempotent (now reports 0 targets). - Verified: re-query → 15/15 invoice maps now carry
invoiceNumber; all other map fields intact. The Records → Coaching Invoices tab now has data to render (no code change / deploy needed — the endpoint already readsinvoice.invoiceNumber). - Latent follow-up (NOT bundled, flagged): the reconstruction fallback in
lib/accounting/reports.server.ts:~1685+lib/accounting/derivedJournals.ts:~529readssessionData.studentAbbr/sessionOrdinal/sessionYear, which T-079 moved into the invoice map — so it would emit…/UNK-…if ever hit. The backfill neutralises it for these 15 (real field present now), but the fallback is still latent-buggy for any future numberless session. Worth a small hardening pass (point it at the invoice-map fields) — left separate to keep this fix minimal. - Update (2026-06-24, Accounting (Infrastructure)): taken on as T-112 — shared helper
lib/accounting/coachingInvoiceNumber.tsintroduced and both call sites switched to it. - Update (2026-06-24, Accounting (Infrastructure)): the second of those two call sites
(
lib/accounting/derivedJournals.ts) was a client-side parallel mirror that was never wired in live code (zero callers). Deleted along with its siblinglib/accounting/reports.tsunder T-079's second postscript — the last legacySessions/{id}.collection('invoice')read in the repo is now gone.