Skip to content

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