Skip to content

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.ts correctly maps gl.coachingInvoices to matchedCoachingInvoices.
  • generateCoachingPaymentDisplayName() did not accept or pass invoiceNumber / invoiceNumbers into the coaching_invoice template data, so {{invoiceNumber}} rendered blank.
  • TransactionDetailsDrawer had preview/token logic for project invoices and coaching payments, but not coaching invoices.
  • AttachmentsPanel counted 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 for sessionDate. After T-079, canonical sessions live at Students/{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.ts now accepts invoiceNumber and invoiceNumbers for the coaching template category and stores them in titleData.values.
  • lib/accounting/transactions.ts now feeds those invoice-number values from matchedCoachingInvoices / validatedInvoices, and uses findSessionRef() for migrated session date reads.
  • lib/accounting/clientTemplateRenderer.ts now supports the same coaching invoice tokens for client previews.
  • components/accounting/transactionWorkspace/TransactionDetailsDrawer.tsx now exposes coaching invoice token chips and previews the coaching invoice template with invoice-number data.
  • components/accounting/transactionWorkspace/AttachmentsPanel.tsx now counts and renders matched coaching invoices in Related Billing Documentation with View and PDF actions.
  • firestore.indexes.json now preserves the existing legacy invoice.invoiceNumber field override and declares the migrated Sessions.invoice.invoiceNumber collection-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 false passed. 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 persisted displayName/titleData, confirming the issue is read-time rendering.
  • Index operation: created the tebs-mel Sessions.invoice.invoiceNumber collection-group ASC field override via the Firestore Admin API; operation returned INITIALIZING. 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 signal main 🟒 Β· nightly πŸ”΄ β†’ main only; mirrored to taskboard.
  • Source (edit): Accounting (Infrastructure) Β· https://claude.ai/code/session_015P6KzVYsQCLgEmUjR9bMwM
  • Bug: lib/accounting/transactions.ts:738 built the coaching session-date as invoice?.sessionDate || sd?.date || … and then only handled the Firestore-Timestamp case (if (dateField?.toDate)). When a migrated invoice stored sessionDate as an ISO string or JS Date (a shape this codebase explicitly allows β€” see coachingWopcAuto.ts / coaching invoices pdf.ts), the truthy non-Timestamp value won the || and then failed the .toDate check, so {{sessionDate}} rendered blank even though the session doc's own date Timestamp (next in the fallback chain) was valid. Graceful (no crash) but a real regression vs. the pre-T-129 read.
  • Fix: replaced the ||-then-.toDate pattern with a tolerant per-candidate parser (Timestamp .toDate / JS Date / _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 unparseable invoice.sessionDate short-circuit the session-doc fallback.
  • Verified: npx tsc --noEmit clean; 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.

  • 0107d3f 2026-06-29 β€” fix(T-129 follow-up): coaching {{sessionDate}} blank when invoice.sessionDate is a string
  • a56f444 2026-06-29 β€” docs: refresh Accounting (Infrastructure) session ledger + T-130 review note (mentions only)