uid: T-180 title: Separate Receipts from Service Invoices; stop the unified expense view being a front door status: done area: accounting created: 2026-07-25 updated: 2026-07-27 owner: girafeev1 related: T-178
T-180 β Receipts and Service Invoices are different things, housed separately¶
Why¶
The owner settled a long-running question on 2026-07-25, verbatim:
"Receipts are for reimbursements, Service Invoices are for charges already made on the company account or card, so Service Invoices and Receipts should be separated and not housed under the same tap"
That is a definition, not a layout preference: it makes "who paid" intrinsic to the document type rather than something a bookkeeper infers afterwards. T-182 depends on it.
What already exists (verified 2026-07-25)¶
Both surfaces are already built and already separate:
components/records/ReceiptsTab.tsx+ReceiptUploadModal.tsxβ the reimbursement pipelinecomponents/records/VendorInvoicesTab.tsx+VendorInvoiceUploadModal.tsx+lib/accounting/serviceInvoiceDrive.server.ts(whose comment reads "Records β Service Invoices upload modal")
What blurred them is a third thing: components/records/ExpenseRecordsTab.tsx and the unified
GET /api/expense-records feed (lib/accounting/vendorInvoiceFeed.server.ts), which normalises
Workspace + GCP + manual invoices into one list. So this task is mostly subtraction, not new build.
Scope¶
- Separate the unified view, per the owner (2026-07-25: "the unified view should be separated as mentioned previously"). Nothing gets submitted through a combined surface.
- Decide what the combined feed is still good for. Open question: it may keep earning its place as a read-only reporting lens across both types β but it must not be a front door.
- Keep both tabs' navigation, empty states and terminology consistent with the new definition.
Non-goals¶
The Service Invoice upload redesign is T-181. Declaring payment method at submission is T-182.
Log¶
2026-07-27 β DONE: two top-level tabs; the merged surface retired¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-180. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
Owner confirmations: "'separate tabs', noted" (2026-07-27), settling the framing question this task carried, and "Let's proceed with our previously created tasks."
Correction to this task's own premise first. The "What already exists" section above says both
surfaces "are already built and already separate" as ReceiptsTab.tsx / VendorInvoicesTab.tsx. Reading
the mounts shows otherwise: those two components are unmounted legacy β T-134 B retired them and
replaced both with ONE merged expense-records tab (ExpenseRecordsTab), an inline
Receipts|Service-Invoices toggle in the tab label, one combined feed, and a toolbar Upload button that
forked on the toggle. So this task was not "mostly subtraction" of a third view sitting beside two live
tabs; it was un-merging the only live surface. The deep-link remap in RecordsApp was the fossil
record: ?tab=receipts / ?tab=vendor-invoices were top-level once, got remapped into the merged tab,
and are now real again.
What shipped
components/records/RecordsApp.tsxβ the merged tab is replaced by two top-level tabs: Receipts (n) and Service Invoices (m), ordered Project & Coaching | Receipts | Service Invoices | WOPCs | Bank Statements | Employer's Returns. The morphing sub-tab buttons and their animation are gone from this tab (the Project & Coaching tab still uses the shared keyframe, so it stays). Upload, Refresh, Export CSV and the export-preview count all route per-tab; the Receipts tab can only ever open the receipt uploader, the Service Invoices tab only the PDF invoice uploader.components/records/ExpenseRecordsTab.tsxβ mounted twice, each with a fixedsubKind. Each instance fetches only its own kind (GET /api/expense-records?kind=β¦) and mounts only its own upload modal β the other kind's pipeline is not merely hidden, it is absent from the tree, which is what makes "who paid is intrinsic to the document type" structural rather than cosmetic.GET /api/expense-recordsβ acceptskind=receipt|service-invoice(400 on anything else); kind-less keeps returning the combined feed.listExpenseRecordsgainedincludeReceiptsbecausereceiptDatabases: []falls back to defaults rather than meaning "none" β an explicit flag, not a sentinel.- Deep links:
?tab=receiptsis native again;?tab=expense-records(merged era) βreceipts;?tab=vendor-invoices(retired key) βservice-invoices. The one live minter of the retired key (lib/notifications/notify.tsvendor-invoice notification) now mintsservice-invoicesdirectly.
The open question this task carried, answered. "What is the combined feed still good for?" β it survives kind-less as a data layer only. No user-facing surface renders a combined expense view any more, and nothing can submit through one. If a cross-type reporting view is ever wanted, the endpoint is ready; building a screen for it would be a new decision.
Verified. Unfiltered tsc --noEmit --incremental false clean Β· vitest run 738 passed (the same 4
known pre-existing billing-ingest failures) Β· eslint on the five changed files: every error
pre-existing, verified by an identical error count (20) on a stashed clean HEAD β none introduced.
Known pre-existing gap ~~, NOT fixed here~~ β fixed in the follow-up entry below: notification
links carry &open=<receiptId>, but neither the merged tab nor the split tabs honoured an open param
(only the retired legacy ReceiptsTab honoured ?id=). β superseded same-day by the follow-up below.
Edit-out signed: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
2026-07-27 β Follow-up: ?open= deep links honoured (owner: "shall we fix it?" β fixed)¶
β Read AGENTS.md Β· checked the board by scope (no dup) Β· tracking T-180. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
ExpenseRecordsTab now consumes ?open=<id> once, on first mount, only in the instance whose tab the
?tab= deep link selected β both instances render (forceRender), so the inactive one burns its
one-shot flag without consuming; honouring the param from the wrong instance would toast "not found" for
a document of the other kind. Receipts open ReceiptDetailDrawer directly by id; service invoices go
through the existing openVendorInvoiceDrawer (lazy vendor-row fetch, its own not-found message). A
notification link like /records?tab=service-invoices&open=<docId> now lands on the right tab and
opens the right drawer β for the first time since the T-134 merger.
Verified: tsc clean Β· vitest 738 passed (same 4 known) Β· eslint back to the file's 3 pre-existing
errors (an added react-hooks disable-comment was itself flagged "rule not found" β this config has no
react-hooks plugin β so it was dropped rather than shipped as a fourth).
Blast radius. The Records page's tab keys changed: anything outside this repo linking
?tab=expense-records still lands correctly via the remap. ReceiptsTab.tsx / VendorInvoicesTab.tsx
remain unmounted legacy whose types are imported by live drawers β deleting them needs a type
extraction first; left as cleanup for whoever next touches the drawers. T-181 (Service Invoice
pipeline) now has a clean single-kind surface to build in.
Commit SHAs (append-only): the T-180 code+board commit in PR #933 (recorded on the PR; this file rides the same commit).
Source¶
Split out of T-178 on 2026-07-25 at the owner's request β verbatim: "Please, if you feel the need, separate the tasks into multiple more tasks as I feel like this one/ two tasks with so many different things might come off as confusing (it's not a bill passed in the senate)." T-178 keeps the reimbursement payment pipeline (WOPC document, Reimbursements tab, issuance); everything about how expense documents are submitted and housed moved here.
Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- 2026-07-28 β Merged to
mainin PR #933 (f2d310a), not deployed. SHA list gainscbe41f5(Codex round) and the merge SHA. Deploy verification pending per the task's own notes. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD