Skip to content

T 098


uid: T-098 title: Firebase Storage retirement β€” bucket inventory endpoint + extend migrate-to-drive to workspace_invoice / gcp_invoice status: done area: infra, records created: 2026-06-17 updated: 2026-06-23 related: T-069 commit: 5ab5d621Β·4a585ff9 (origin/nightly via cherry-pick; PR #737)


Why

Direct follow-up to T-069 β€” that task closed the Service Invoice (vendor_invoice) migration to Drive, but left two gaps the survey surfaced:

  1. No bucket-side inventory. file-storage-survey only sees Firestore-tracked records. Anything the bucket holds that isn't referenced by a file-archive storagePath is invisible β€” including orphans from feature code paths that bypass the file-archive.
  2. Migration only handled vendor_invoice. The 44 legacy workspace_invoice records the survey surfaced (plus any gcp_invoice legacy records) had no migration path.

Closing both gaps is what lets Firebase Storage be fully retired rather than partially decommissioned.

What changed

  • GET /api/admin/firebase-storage-inventory β€” scans the bucket directly (not via Firestore). Reports total count/bytes, byPrefix breakdown (catches any non-receipts/ paths still in use), and the tracked vs orphan split with the first 20 orphan paths surfaced for spot-check.
  • POST /api/admin/migrate-to-drive now handles workspace_invoice + gcp_invoice. The auto-ingest path uses 14b. Vendor Invoices/{YYYY}/ on the same accounting tree.
  • New shared module lib/accounting/vendorInvoiceDrive.server.ts β€” extracted buildVendorInvoiceFilename / resolveVendorInvoicesFolderId / VENDOR_INVOICE_DRIVE_PURPOSE from pages/api/workspace/billing/ingest.ts so the auto-ingest path and the admin migrate path read from one source of truth (no drift on filename/folder conventions).
  • pages/api/workspace/billing/ingest.ts now imports from the new module.

Supported migration types after this change:

Type Drive folder Filename label
vendor_invoice (Service Invoice) 14d. Service Invoices/{Category}/ Invoice_Service
workspace_invoice 14b. Vendor Invoices/{YYYY}/ WorkspaceInvoice
gcp_invoice 14b. Vendor Invoices/{YYYY}/ GcpInvoice
gcp_statement (extended during merge) 14b. Vendor Invoices/{YYYY}/ GcpStatement

Decision log

2026-06-23 β€” opened + landed (cherry-picked from PR #737)

  • βœ… Attestation (Records (Infrastructure)): read AGENTS.md; scope-scanned tasks β€” T-069 is the predecessor (Service Invoice migration; closed done 2026-06-19) but this work is scope extension (workspace_invoice + gcp_invoice migration + bucket inventory), so opening a new task rather than re-opening T-069's closed-loop entry. No other covering task on the board.
  • Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
  • Owner directive (verbatim, 2026-06-23):

    "If the task included in the PRs are not tasks created and listed on the Task Board, do that, if they are, but you have not signed to those tasks, sign them according to the instruction on AGENTS.MD, while proceed to work on the still open PRs"

  • What landed (origin/nightly):
  • 5ab5d621 (was ce9fc14e in PR #737) β€” firebase-storage-inventory endpoint + extend migrate-to-drive + extract vendorInvoiceDrive.server.ts. Conflict resolution at merge: nightly had grown gcp_statement support since the PR was written. Extended the new vendorInvoiceDrive.server.ts to know the gcp_statement vendor (widened the AutoVendor union and added the GcpStatement label branch); kept ingest.ts's gcp_statement description special-case ("Google Cloud (statement) …" without the word "invoice"). PR's structural refactor preserved.
  • 4a585ff9 (was ca9f0e09) β€” admin migrate endpoint now reads invoiceCsv.* for the new auto-ingested types (workspace_invoice / gcp_invoice). Cherry-pick was clean.
  • PR #737 closed with a pointer to these two SHAs.
  • Branch policy: committed straight to nightly per the 🟒 lockstep operational signal in AGENTS.md; main to be FF'd to nightly's tip in the same push window.
  • Blast radius: adds two admin-only API endpoints + the new shared helper module; touches the ingest path's filename/folder helper but does not change the convention (same filename, same folder, same SA). Live behavior of the Workspace + GCP billing scrapes is unchanged. The gcp_statement ingest path was preserved verbatim during the conflict resolution.
  • Verification plan (post-deploy):
  • GET /api/admin/firebase-storage-inventory β†’ expect a non-zero total, byPrefix showing receipts/ dominant, an orphans count + sample list.
  • POST /api/admin/migrate-to-drive { docId: "<a workspace_invoice id>", trashOriginal: false } β†’ expect 200 with newStoragePath: drive:…, file lands under 14. Expenses Records/{YYYY}/14b. Vendor Invoices/ with ERL_WorkspaceInvoice_…pdf. Re-call β†’ skipped: 'already-on-drive'.
  • Re-survey β†’ that workspace_invoice row moves from firebase-storage to drive.