Skip to content

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 pipeline
  • components/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 fixed subKind. 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 β€” accepts kind=receipt|service-invoice (400 on anything else); kind-less keeps returning the combined feed. listExpenseRecords gained includeReceipts because receiptDatabases: [] falls back to defaults rather than meaning "none" β€” an explicit flag, not a sentinel.
  • Deep links: ?tab=receipts is 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.ts vendor-invoice notification) now mints service-invoices directly.

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

βœ… 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 main in PR #933 (f2d310a), not deployed. SHA list gains cbe41f5 (Codex round) and the merge SHA. Deploy verification pending per the task's own notes. Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD