feat(orders): show the receipt in the transaction detail panel
ci / lint-test (push) Failing after 47s
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user