diff --git a/src/__tests__/integration/queries.test.ts b/src/__tests__/integration/queries.test.ts
index 0ee01ac..64e41c9 100644
--- a/src/__tests__/integration/queries.test.ts
+++ b/src/__tests__/integration/queries.test.ts
@@ -253,3 +253,55 @@ describe("getParticipantBalances", () => {
expect(bobBalance!.unsettled_count).toBe(2);
});
});
+
+describe("getTransactions — order provenance for the description sub-line", () => {
+ it("carries the route and platform of an order-derived row", async () => {
+ // Five rows all reading "Order - Uber Trip" are indistinguishable in the
+ // list; where the trip went is the only thing that separates them, and it
+ // was already stored.
+ const { ownerId } = await seedParticipants(pool);
+ const txId = await insertTransaction(pool, ownerId, { description: "Order - Uber Trip" });
+ await pool.query(
+ `INSERT INTO expense_metadata (source, order_reference, platform, route, transaction_id)
+ VALUES ('email', $1, 'uber',
+ '[{"label":"Pick-up","time":"7:32 pm","address":"Terminal 2, Melbourne Airport (MEL), Tullamarine VIC 3045, Australia"},
+ {"label":"Drop-off","time":"8:10 pm","address":"19 Lady Penrhyn Dr, Wyndham Vale VIC 3024, Australia"}]'::jsonb,
+ $2)`,
+ [`route-${Date.now()}`, txId]
+ );
+
+ const { data } = await getTransactions(ownerId, { limit: 50, offset: 0 });
+ const row = data.find((r) => r.id === txId)!;
+ expect(row.order_platform).toBe("uber");
+ expect(row.order_route).toHaveLength(2);
+ expect(row.order_route![0].address).toContain("Melbourne Airport");
+ });
+
+ it("resolves from the statement line for a card-settled order", async () => {
+ // A card-settled order creates no transaction of its own (I5) — the
+ // receipt points at the statement line through matched_transaction_id.
+ const { ownerId } = await seedParticipants(pool);
+ const txId = await insertTransaction(pool, ownerId, { description: "UBER *TRIP AUCKLAND" });
+ await pool.query(
+ `INSERT INTO expense_metadata (source, order_reference, platform, route, matched_transaction_id)
+ VALUES ('email', $1, 'uber',
+ '[{"label":"Pick-up","time":null,"address":"64 Federal Street, Auckland 1010, NZ"},
+ {"label":"Drop-off","time":null,"address":"International Terminal, Auckland 2022, New Zealand"}]'::jsonb,
+ $2)`,
+ [`route-card-${Date.now()}`, txId]
+ );
+
+ const { data } = await getTransactions(ownerId, { limit: 50, offset: 0 });
+ const row = data.find((r) => r.id === txId)!;
+ expect(row.order_route).toHaveLength(2);
+ });
+
+ it("leaves an ordinary transaction with no route", async () => {
+ const { ownerId } = await seedParticipants(pool);
+ const txId = await insertTransaction(pool, ownerId, { description: "COLES 1234" });
+ const { data } = await getTransactions(ownerId, { limit: 50, offset: 0 });
+ const row = data.find((r) => r.id === txId)!;
+ expect(row.order_route).toBeNull();
+ expect(row.order_platform).toBeNull();
+ });
+});
diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx
index 3603516..44acc72 100644
--- a/src/app/transactions/page.tsx
+++ b/src/app/transactions/page.tsx
@@ -9,7 +9,7 @@ import { TagPicker } from "@/components/tag-picker";
import { AddTransactionModal } from "@/components/add-transaction-modal";
import { EditTransactionModal } from "@/components/edit-transaction-modal";
import { CsvImportModal } from "@/components/csv-import-modal";
-import type { TransactionRow } from "@/lib/queries";
+import type { TransactionRow, RoutePointRow } from "@/lib/queries";
import type { RuleRow } from "@/lib/hooks";
function formatDate(d: string) {
@@ -475,6 +475,25 @@ function MultiSelect({
);
}
+/**
+ * "Melbourne Airport (MEL) → Wyndham Vale VIC 3024" from the two stops on an
+ * Uber *trip* receipt. Deliveries are excluded by the caller: their merchant
+ * already identifies them, so the restaurant's street address would be clutter
+ * on every food order.
+ *
+ * Keeps the first two comma-segments of each address — a truncation, not a
+ * guess about geography. The venue or street comes first in Uber's format and
+ * is the identifying part; the full text stays in the title attribute.
+ */
+function routeSummary(route: RoutePointRow[] | null | undefined): string | null {
+ if (!route || route.length < 2) return null;
+ const short = (a: string) => a.split(",").slice(0, 2).join(",").trim();
+ const from = short(route[0].address);
+ const to = short(route[route.length - 1].address);
+ if (!from || !to) return null;
+ return `${from} → ${to}`;
+}
+
export default function TransactionsPage() {
return (
{t.description}
- {t.notes && ( + {t.notes ? ({t.notes}
+ ) : t.order_platform === "uber" && routeSummary(t.order_route) && ( + // Five rows all reading "Order - Uber Trip" are + // indistinguishable. Where the trip went is what tells + // them apart, and it was already stored. A note the user + // wrote always wins — this only fills an empty line. +`${r.label}${r.time ? ` ${r.time}` : ""}: ${r.address}`).join("\n")} + > + {routeSummary(t.order_route)} +
)}