I 013
tid: I-013 title: Service Invoice register XLSX β Invoice Number hyperlink dies within ~1 hour of export status: fixed source: Records β Receipts: Service Invoice β Export β click on Invoice Number β owner report 2026-06-25 area: accounting-records task: T-114 created: 2026-06-25 updated: 2026-06-25
Symptom¶
The owner exported a Service Invoice register XLSX and clicked the hyperlink attached to the Invoice Number β it didn't open.
Owner, 2026-06-25 (verbatim): "I have just exported this file (From Records > Receipts: Service Invoice > Export), and the hyperlink URL attached to the invoice number doesn't work. Can you help me look into why?"
Root cause (proven against the uploaded export)¶
The exported workbook (ERL_Register_Invoice_Service_20260625.xlsx, 71 hyperlinks) carries two URL shapes:
| Count | URL shape | Lifetime |
|---|---|---|
| 25 | /api/accounting/receipts/<id>?redirect=pdf (same-origin) |
durable β no expiry, auth at click time |
| 45 | https://storage.googleapis.com/aote-pms.firebasestorage.app/... with Expires= |
1 hour β all 45 share the SAME Expires=1782357996 |
| 1 | (blank) | n/a |
The 45 short-lived URLs come from vendorInvoiceFeed.resolveVendorPdfUrl calling getReceiptDownloadUrl for legacy Firebase-Storage records (1-hour signed GCS URL). The export wrote that ephemeral URL verbatim into the workbook. Within the first hour: works. After: dead. The owner's click landed on one already past expiry.
curl confirmed the GCS URL itself is currently live (HTTP/2 200, application/pdf) β it's not bad signing or wrong bucket; it's "short-lived URL in a long-lived artifact."
Resolution¶
Structural β escalated to T-114 (fixed there): export now writes the same-origin streaming URL for every record, regardless of underlying storage. The streaming endpoint resolves Drive bytes or mints a fresh GCS signed URL at click time, so workbooks never carry expiring URLs.
Sibling not addressed¶
The Receipts XLSX export uses a separate File column (Click Here linking to r.fileUrl) and would carry the same class of bug for any legacy-GCS receipt. Deliberately not changed in this PR to keep the diff focused on the reported symptom β but if the receipts export shows the same problem, extend the same accessor pattern (route through /api/accounting/receipts/<recordId>?redirect=pdf).