feat(orders): show the receipt in the transaction detail panel
ci / lint-test (push) Failing after 47s

`expense_metadata` has held the itemised receipt since ingestion started and
nothing in the UI ever read it. A transaction that came from a DoorDash or Uber
Eats receipt showed a merchant and an amount, with the item list and the
delivery addresses sitting unread in the row behind it (user, 2026-07-27).

Adds GET /api/transactions/[id]/order and an "Order details" section in the
edit modal: line items with their options, pick-up/delivery stops with times
and addresses, the card tail when one was involved, and the provider's own
order reference.

Two details that matter:

- The lookup resolves from **both** sides — `transaction_id` OR
  `matched_transaction_id`. A card-settled order creates no transaction of its
  own (I5); the receipt points at the statement line instead. Matching only on
  transaction_id would have left the panel blank on exactly the card-paid
  orders, which are the ones whose detail is hardest to find elsewhere.
- An empty item list says so in words rather than rendering nothing. Uber
  itemises groceries but not restaurant orders, and orders ingested before the
  Uber item parser existed have none either — a blank section reads as a bug
  when it is usually the receipt.

Read-only. This is what a provider sent; editing it would make provenance mean
nothing.
This commit is contained in:
2026-07-27 10:55:57 +10:00
parent df4b875b82
commit b6cd62f7b5
4 changed files with 220 additions and 0 deletions
+27
View File
@@ -250,6 +250,33 @@ export function useTransactionSplits(transactionId: number) {
});
}
export interface OrderReceipt {
platform: "doordash" | "ubereats" | "uber" | null;
order_reference: string | null;
line_items: { qty: number; description: string; amount: number; options?: string[] }[];
route: { label: string; time: string | null; address: string }[];
subtotal: string | null;
amount: string | null;
currency: string | null;
card_last4: string | null;
flags: string[];
source_email_subject: string | null;
transaction_date: string | null;
}
/** The receipt behind a transaction, or null when it did not come from one. */
export function useOrderReceipt(transactionId: number) {
return useQuery<OrderReceipt | null>({
queryKey: ["order-receipt", transactionId],
queryFn: async () => {
const res = await fetch(`/api/transactions/${transactionId}/order`);
if (!res.ok) return null;
return res.json();
},
staleTime: Infinity, // a receipt never changes
});
}
export function useSetSplits() {
const qc = useQueryClient();
return useMutation({