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:
- No bucket-side inventory.
file-storage-surveyonly sees Firestore-tracked records. Anything the bucket holds that isn't referenced by afile-archivestoragePathis invisible β including orphans from feature code paths that bypass the file-archive. - Migration only handled
vendor_invoice. The 44 legacyworkspace_invoicerecords the survey surfaced (plus anygcp_invoicelegacy 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,byPrefixbreakdown (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-drivenow handlesworkspace_invoice+gcp_invoice. The auto-ingest path uses14b. Vendor Invoices/{YYYY}/on the same accounting tree.- New shared module
lib/accounting/vendorInvoiceDrive.server.tsβ extractedbuildVendorInvoiceFilename/resolveVendorInvoicesFolderId/VENDOR_INVOICE_DRIVE_PURPOSEfrompages/api/workspace/billing/ingest.tsso 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.tsnow 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; closeddone2026-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(wasce9fc14ein PR #737) βfirebase-storage-inventoryendpoint + extendmigrate-to-drive+ extractvendorInvoiceDrive.server.ts. Conflict resolution at merge: nightly had growngcp_statementsupport since the PR was written. Extended the newvendorInvoiceDrive.server.tsto know thegcp_statementvendor (widened theAutoVendorunion and added theGcpStatementlabel branch); keptingest.ts'sgcp_statementdescription special-case ("Google Cloud (statement) β¦" without the word "invoice"). PR's structural refactor preserved.4a585ff9(wasca9f0e09) β admin migrate endpoint now readsinvoiceCsv.*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
nightlyper the π’ lockstep operational signal inAGENTS.md;mainto 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_statementingest path was preserved verbatim during the conflict resolution. - Verification plan (post-deploy):
GET /api/admin/firebase-storage-inventoryβ expect a non-zerototal,byPrefixshowingreceipts/dominant, anorphanscount + sample list.POST /api/admin/migrate-to-drive { docId: "<a workspace_invoice id>", trashOriginal: false }β expect 200 withnewStoragePath: drive:β¦, file lands under14. Expenses Records/{YYYY}/14b. Vendor Invoices/withERL_WorkspaceInvoice_β¦pdf. Re-call βskipped: 'already-on-drive'.- Re-survey β that
workspace_invoicerow moves fromfirebase-storagetodrive.