T 129
uid: T-129 title: GL 4001 coaching-invoice matched transactions β restore invoice-number title tokens + related docs status: done area: accounting-records created: 2026-06-26 updated: 2026-06-26 related: T-079, T-112, T-118
Why¶
The owner reported that bank transactions matched to Coaching Invoice / GL 4001 still did not display the invoice number, even though the display-name template was configured for it, and Transaction Details β Related Billing Documentation showed no linked coaching invoice.
Owner direction, 2026-06-26:
"Transactions that are matched to a Coaching Invoice (GL 4001), the display name template is configured for the transaction to show the invoice number, but the invoice number is not shown and Related Billing Documentation in the Transaction Details modal isn't showing anything. I think it's quite certain that the web app is still not reading the recently migrated Coaching Invoice information correctly."
Follow-up direction, 2026-06-26:
"Can you also help me resolve the previous coachingInvoice issue as well?"
Diagnosis¶
Live Firestore (tebs-erl/accounting/transactions/entries) has 7 transactions with
gl.coachingInvoices.{invoiceNumber} allocations. Those docs already carry the invoice number as the
map key and the allocation carries sessionId, studentAbbr, studentName, and sessionOrdinal.
Example shape:
gl: {
coachingInvoices: {
"MEL-2026/Nan-020(PR)": [{
amount: 1500,
sessionId: "lche21ljm2jds6cpkn7h50q0p4",
studentAbbr: "Nan",
studentName: "Nancy Kwai",
sessionOrdinal: 20
}]
}
}
So the data was not absent. The read path dropped it:
lib/accounting/transactionAdapter.tscorrectly mapsgl.coachingInvoicestomatchedCoachingInvoices.generateCoachingPaymentDisplayName()did not accept or passinvoiceNumber/invoiceNumbersinto thecoaching_invoicetemplate data, so{{invoiceNumber}}rendered blank.TransactionDetailsDrawerhad preview/token logic for project invoices and coaching payments, but not coaching invoices.AttachmentsPanelcounted and rendered project invoices + WOPCs only, so coaching invoice matches looked like they had no Related Billing Documentation.- The server-side enrichment path also read
tebs-mel/Sessions/{sessionId}directly forsessionDate. After T-079, canonical sessions live atStudents/{studentAbbr}/Sessions/{sessionId}.
Separate live finding: collectionGroup('Sessions').where('invoice.invoiceNumber', '==', ...)
failed without the collection-group single-field index on invoice.invoiceNumber. The display fix does
not depend on that query because bank transactions already carry the invoice number and session ID, but
lookup-by-invoice-number paths still need the index promised by T-079.
What Shipped¶
lib/accounting/displayNameGenerator.tsnow acceptsinvoiceNumberandinvoiceNumbersfor the coaching template category and stores them intitleData.values.lib/accounting/transactions.tsnow feeds those invoice-number values frommatchedCoachingInvoices/validatedInvoices, and usesfindSessionRef()for migrated session date reads.lib/accounting/clientTemplateRenderer.tsnow supports the same coaching invoice tokens for client previews.components/accounting/transactionWorkspace/TransactionDetailsDrawer.tsxnow exposes coaching invoice token chips and previews the coaching invoice template with invoice-number data.components/accounting/transactionWorkspace/AttachmentsPanel.tsxnow counts and renders matched coaching invoices in Related Billing Documentation with View and PDF actions.firestore.indexes.jsonnow preserves the existing legacyinvoice.invoiceNumberfield override and declares the migratedSessions.invoice.invoiceNumbercollection-group field override.
Decision Log¶
2026-06-26 β opened + shipped¶
- β
Attestation (Codex): read
AGENTS.md; checked the task board by scope, not UID. T-079 and T-112 cover migration/backfill; T-118 only audited doc-type tokens generally. No existing task covered GL 4001 transaction detail rendering + related-document display. - Source: Codex Β· local desktop session Β· 2026-06-26.
- Tests:
NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit --pretty falsepassed. The first default-heap run OOMed at ~2GB before producing diagnostics. A direct Node smoke test of the exported TS API was blocked by the local TS/ESM loader, not app code. - Live data audit: read 7 live transactions with
gl.coachingInvoices; all had invoice-number map keys and no persisteddisplayName/titleData, confirming the issue is read-time rendering. - Index operation: created the
tebs-melSessions.invoice.invoiceNumbercollection-group ASC field override via the Firestore Admin API; operation returnedINITIALIZING. A second patch restored collection-scope ASC/DESC/CONTAINS indexes for the same field so normal single-collection behavior stays intact. - Blast radius: read/render only. No Firestore writes. Affects Accounting bank transaction row titles, Transaction Details title preview/token chips, the Related Billing Documentation card, and one Firestore field-index declaration.
2026-06-29 β follow-up fix: {{sessionDate}} blank-token regression (Accounting (Infrastructure))¶
- β
Attestation (Accounting (Infrastructure)): read
AGENTS.md; appending a signed follow-up to Codex's task (not editing the prose above) after an owner-requested review of the Codex commits. Branch signalmainπ’ Β·nightlyπ΄ β main only; mirrored totaskboard. - Source (edit): Accounting (Infrastructure) Β· https://claude.ai/code/session_015P6KzVYsQCLgEmUjR9bMwM
- Bug:
lib/accounting/transactions.ts:738built the coaching session-date asinvoice?.sessionDate || sd?.date || β¦and then only handled the Firestore-Timestamp case (if (dateField?.toDate)). When a migrated invoice storedsessionDateas an ISO string or JSDate(a shape this codebase explicitly allows β seecoachingWopcAuto.ts/coaching invoices pdf.ts), the truthy non-Timestamp value won the||and then failed the.toDatecheck, so{{sessionDate}}rendered blank even though the session doc's owndateTimestamp (next in the fallback chain) was valid. Graceful (no crash) but a real regression vs. the pre-T-129 read. - Fix: replaced the
||-then-.toDatepattern with a tolerant per-candidate parser (Timestamp.toDate/ JSDate/_seconds|seconds/ ISO string|number) that maps every candidate and takes the first yielding a valid date β preserving T-129's invoice-map-first priority while no longer letting an unparseableinvoice.sessionDateshort-circuit the session-doc fallback. - Verified:
npx tsc --noEmitclean; accounting vitest 127/127 pass. Display/render-only; no data or schema change.
Commit index (backfilled 2026-07-01, best-effort Β· Coaching (Diagnostic))¶
Candidate related commits, auto-backfilled from git on main: commits whose message references this task's UID or a PR number it cites. Not verified β this squash-merged history can't yield a precise per-task list, so rows tagged (mentions only) name the task in passing (may be tangential) and untagged work commits may be missing. Treat as a starting point: verify, prune tangential rows, and append any real ones per the AGENTS.md "record every related SHA" policy.
0107d3f2026-06-29 β fix(T-129 follow-up): coaching {{sessionDate}} blank when invoice.sessionDate is a stringa56f4442026-06-29 β docs: refresh Accounting (Infrastructure) session ledger + T-130 review note (mentions only)