I-021 — Coaching share links 404: findSessionRef throws on collection-group documentId()¶
Symptoms¶
In the exported Register of Coaching Invoice XLSX, clicking an invoice-number
hyperlink opened the no-login viewer (/v/<shortId>) but always showed
"Document not found" — for every coaching invoice. (Project-invoice links on
the sibling register work.)
Root cause¶
findSessionRef(coachingDb, sessionId) (lib/accounting/coachingSessions.ts),
when called without a studentAbbr, ran a "fast path":
coachingDb.collectionGroup('Sessions')
.where(FieldPath.documentId(), '==', sessionId) // sessionId = bare doc id
On a collection-group query the documentId() value must resolve to a valid
document path — i.e. an even number of path segments. Every coaching session
id is a single segment, so Firestore throws:
When querying a collection group and ordering by FieldPath.documentId(), the corresponding value must result in a valid document path, but '…' is not because it contains an odd number of segments.
The code's comment assumed this filter merely missed nested sessions and fell
through to the exhaustive scan below it. It does not — it throws, uncaught.
The throw propagated findSessionRef → getCoachingInvoiceForSession →
fetchSharedCoachingInvoice, which /v/[id].tsx catches as not-found.
Because the throw is in the shared resolver, it also broke every other
no-studentAbbr caller: the coaching-invoice PDF endpoint
(/api/coaching/invoices/[sessionId]/pdf), the coaching ZIP download
(/api/records/coaching-invoices/download-zip), …/saved.ts, and a coaching
path in lib/accounting/transactions.ts. The Records listing tab was unaffected
because the matchable-coaching-invoices API reads doc.id directly with no
documentId() filter.
(Likely surfaced "now" by a @google-cloud/firestore version that hard-throws on
this filter rather than returning empty.)
Fix (commit below)¶
lib/accounting/coachingSessions.ts — replace the throwing collection-group
documentId() "fast path" with a cheap direct read of a legacy top-level
Sessions/{id} doc (collection('Sessions').doc(sessionId).get()), then fall to
the existing exhaustive collectionGroup('Sessions') scan (match by trailing
doc.id) for nested Students/{abbr}/Sessions/{id} sessions resolved without a
studentAbbr. Removed the now-unused FieldPath import.
Verification¶
Ran the fixed resolver against live tebs-mel: 15/15 invoiced coaching
sessions now resolve (all via the scan path, ~0.4–1.7 s each — acceptable for the
low-rate share-link / PDF / ZIP viewers); before the fix all 15 threw. Project-
pinned tsc — clean. Runtime confirmation is owner-on-deploy (re-export the
Coaching register, click an invoice-number link).
Decision log¶
2026-06-30 — ✅ Read AGENTS.md before working this ticket. Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Owner reported coaching-invoice XLSX links "aren't working" right after the I-020 service-invoice fix. Ground-truthed against live tebs-mel — the documentId() collection-group filter threw for every (single-segment) session id, not just missed. Fix is in the shared resolver so it repairs the share viewer AND the coaching PDF/ZIP endpoints at once. Ticket-only (surgical resolver fix; no structural T-task).