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
@@ -437,3 +437,65 @@ describe("how an ingested order presents in the app", () => {
expect(row!.route.map((r) => r.label)).toEqual(["Pick-up", "Delivery"]);
});
});
describe("receipt lookup for the transaction detail panel", () => {
// Mirrors /api/transactions/[id]/order — the panel resolves a receipt from
// either side, and a card-settled order only has the matched_transaction_id
// side, which is exactly where the detail would otherwise go missing.
const receiptFor = (txnId: number) =>
queryRow<{ platform: string; route: { label: string }[] }>(
`SELECT platform, route FROM expense_metadata
WHERE transaction_id = $1 OR matched_transaction_id = $1 LIMIT 1`,
[txnId]
);
it("finds the receipt for a credits order", async () => {
// ue-00 is Uber Cash — credits, so it creates a transaction. ue-09 names a
// payer with no instrument and correctly parks awaiting a card statement,
// which would leave nothing to look the receipt up by.
const p = parseOrderHTML(
html("ue-00"),
meta({ messageId: `panel-${Date.now()}`, subject: "Your Wednesday order with Uber Eats", sender: "uber.com" })
);
const res = await processOrderIngestion(p);
const r = await receiptFor(res.transactionId!);
expect(r!.platform).toBe("ubereats");
expect(r!.route.map((x) => x.label)).toEqual(["Pick-up", "Delivery"]);
});
it("finds it from the statement line for a card-settled order", async () => {
const st = await queryRow<{ id: number }>(
`INSERT INTO statements (bank_name, account_number, filename)
VALUES ('CBA', '5523504401723893', 'panel-cba.pdf') RETURNING id`
);
const card = await queryRow<{ id: number }>(
`INSERT INTO transactions (statement_id, transaction_date, description, amount, transaction_type)
VALUES ($1, '2026-04-07', 'UBER *EATS ZURICH', 51.23, 'debit') RETURNING id`,
[st!.id]
);
const m = await queryRow<{ id: number }>(
`INSERT INTO expense_metadata (source, order_reference, platform, route, matched_transaction_id)
VALUES ('email', $1, 'ubereats', '[{"label":"Pick-up","time":null,"address":"Ebikon"}]'::jsonb, $2)
RETURNING id`,
[`panel-card-${Date.now()}`, card!.id]
);
expect(m).not.toBeNull();
const r = await receiptFor(card!.id);
expect(r!.platform).toBe("ubereats");
expect(r!.route[0].address).toBe("Ebikon");
});
it("returns nothing for an ordinary transaction", async () => {
const st = await queryRow<{ id: number }>(
`INSERT INTO statements (bank_name, account_number, filename)
VALUES ('CBA', '1111', 'panel-plain.pdf') RETURNING id`
);
const t = await queryRow<{ id: number }>(
`INSERT INTO transactions (statement_id, transaction_date, description, amount, transaction_type)
VALUES ($1, '2026-04-07', 'COLES 1234', 12.00, 'debit') RETURNING id`,
[st!.id]
);
expect(await receiptFor(t!.id)).toBeNull();
});
});