Skip to content

Records — Service-Invoice ZIP silently drops rows with no file

Why (owner, 2026-06-25)

"When I highlight all 71 service invoices, only 5 was included in the zip"

The Service Invoices sub-tab's bulk-download (right-click → "Download N as ZIP" or the toolbar Download button) silently filtered out every selected row whose fileUrl was null. For service-invoice rows that's common — GCP / Workspace billing rows can exist in the feed BEFORE the NAS scrapes the matching PDF to Drive. The user selected 71 rows and got a 5-file ZIP back with no explanation.

What shipped

components/records/ExpenseRecordsTab.tsx

  • triggerSelectionDownload for the service-invoice sub-kind now splits the selection into eligible (has fileUrl) and skipped (no file filed yet) up front.
  • If ALL selected rows have no file: a clear message.error reports the count and aborts (no empty ZIP, no progress toast).
  • If SOME selected rows have no file: a Modal.confirm explains why (typically GCP/Workspace months whose invoice hasn't been scraped yet) AND shows the eligible count on the confirm button (Download N). User must explicitly accept the truncated set.
  • If every selected row has a file: behaviour is unchanged — straight to runBatchDownload.

The pre-existing per-row progress toast + 429 retry semantics are untouched.

Why this and not "just include all of them"

The PDF stream endpoint /api/accounting/receipts/<id>?redirect=pdf returns 404 for rows without a storagePath, so a 71-attempt batch would emit 66 failure entries into the toast — noisy, AND the user would still get a 5-file ZIP. Surfacing the discrepancy in a single modal up-front is clearer, costs no network traffic, and lets the user cancel if it's not what they expected.

Decision log

2026-06-25 — T-122 opened + shipped

  • Attestation (Records (Infrastructure)): read AGENTS.md; no open task covered the silent-filter case (T-117 added the feature, but didn't surface the partial-eligibility UX). Sequel to T-117.
  • Source: Records (Infrastructure) · https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Owner direction (verbatim, 2026-06-25): quoted above.
  • Tests + tsc: npx tsc --noEmit clean. Live render verification left to the owner — open the Records page → Receipts tab → Service Inv sub-tab → select rows that span both filed + unfiled service invoices → trigger Download. Expect a confirm modal with the eligible count when a subset has no file, an error when none do, and silent download when all do.
  • Blast radius: single component change, behind a per-selection branch; no shared helper / API touched. Receipts sub-kind branch unchanged.

2026-06-28 — follow-up: filed GCS-backed service invoices still failed inside the browser ZIP

  • Attestation (Codex local session): read AGENTS.md; checked the board by scope, not UID — this is the same "selected 71, ZIP only contains 5" Service Invoice ZIP scope, not a new task. Tracking T-122.
  • Source: Codex local session · /Users/gutchumi/dev/ArtifactoftheEstablisher-codex-wopc-pdfjs
  • Owner report (verbatim, 2026-06-28):

    "I have also mentioned previously that I could only download 5 service invoices when I've actually selected 70 on the Records page > Service Invoices tab"

  • Correction to the earlier diagnosis: the first fix only handled rows with no uploaded file. The Chrome log showed a second failure: filed legacy-GCS service invoices were fetched by the browser ZIP worker through /api/accounting/receipts/<id>?redirect=pdf; that endpoint redirected to a signed storage.googleapis.com URL, and the browser then blocked it with CORS. Drive-backed rows streamed same-origin and worked, which is why only a small subset entered the ZIP.
  • What changed: pages/api/accounting/receipts/[id].ts now supports ?stream=pdf, which streams both Drive and GCS bytes through the app origin. components/records/ExpenseRecordsTab.tsx uses that streaming URL for Service Invoice ZIP items, while keeping the old redirect path available for normal iframe/open-file usage.
  • Verification: git diff --check clean; NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmit clean.
  • Blast radius: Records → Receipts → Service Invoice bulk ZIP only, plus the receipt API's new same-origin streaming branch. Existing ?redirect=pdf links remain supported.

2026-06-29 — follow-up: GCP rows used billing month instead of File Archive document id

  • Attestation (Codex local session): read AGENTS.md; checked the board by scope, not UID — this remains the same Records → Service Invoices bulk-ZIP symptom, not a new task. Tracking T-122.
  • Source: Codex local session · /Users/gutchumi/dev/ArtifactoftheEstablisher-codex-wopc-pdfjs
  • Owner handoff (verbatim, 2026-06-29):

    "I had Claude to tackle the below issue, but I'm out of token with them. Can you possibly pick it up?"

  • Correction to the second fix: switching the generated URL to ?stream=pdf fixed CORS for valid document ids, but the Service Invoice ZIP still built that URL from r.recordId. For GCP rows, recordId is the billing month (for example 202606), while the filed PDF lives under a separate File Archive document id already embedded in r.fileUrl.
  • What changed: ZIP item URLs now use r.fileUrl as the source of truth. Same-origin /api/accounting/receipts/<docId>?redirect=pdf links are rewritten to ?stream=pdf for browser ZIP fetches; raw signed URLs are left untouched for legacy rows.
  • Verification: git diff --check clean; NODE_OPTIONS=--max-old-space-size=4096 npx tsc --noEmit clean.
  • Blast radius: Records → Receipts → Service Invoices bulk ZIP only. The row open/preview links still use the original fileUrl.

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.

  • bc1af24 2026-06-25 — fix(records): service-invoice ZIP UX (T-122) + share viewer WOPC/coaching (T-123)
  • 9be22b9 2026-06-25 — Merge: T-122 service-invoice ZIP UX + T-123 share viewer WOPC/coaching fixes
  • 99f9c36 2026-06-25 — feat(records,wpp): unified Bank-Tx letterhead (T-124) + WPP CSV → XLSX (T-125) (mentions only)
  • a98745b 2026-06-25 — Merge main + renumber T-122 → T-126 (renumber-on-merge, parallel-agent T-122/123/124/125 collision)