diff --git a/src/components/order-details.tsx b/src/components/order-details.tsx index 5d300a3..c8094d4 100644 --- a/src/components/order-details.tsx +++ b/src/components/order-details.tsx @@ -13,12 +13,9 @@ import { type OrderReceipt, type OrderRating, } from "@/lib/hooks"; - -const PLATFORM_LABEL: Record = { - doordash: "DoorDash", - ubereats: "Uber Eats", - uber: "Uber", -}; +// One platform vocabulary, shared with the intake cards — three copies had +// already drifted apart (the intake one was missing `uber`). +import { platformLabel } from "@/lib/intake-rules"; /** * Who records verdicts. A two-person household with one primary user: the @@ -99,7 +96,7 @@ export function OrderDetails({

Order details {receipt.platform && ( - {PLATFORM_LABEL[receipt.platform] ?? receipt.platform} + {platformLabel(receipt.platform)} )}

{receipt.card_last4 && ( diff --git a/src/lib/intake-rules.ts b/src/lib/intake-rules.ts index 69c9bc3..398d7bf 100644 --- a/src/lib/intake-rules.ts +++ b/src/lib/intake-rules.ts @@ -43,6 +43,7 @@ export function platformForSource(source: IntakeSource): string | null { export const PLATFORM_LABEL: Record = { ubereats: "Uber Eats", doordash: "DoorDash", + uber: "Uber", }; export function platformLabel(platform: string | null): string { @@ -51,10 +52,16 @@ export function platformLabel(platform: string | null): string { /** * A small pattern list, not an exact lookup (plan §2, Codex #6). Uber Eats prints - * `Coles (Wyndham Vale)`, a PDF may print `Woolworths Group Limited`; substring on - * the normalised name covers both without a merchant master-data subsystem. + * `Coles (Wyndham Vale)`, a PDF may print `Woolworths Group Limited`; a + * word-boundary alternation on the normalised name covers both without a + * merchant master-data subsystem. + * + * The boundaries are load-bearing, not tidiness: a bare substring made + * `Bigals Pizza` a grocer, because "b-IGA-ls" contains `iga`. Short names in a + * substring list collide with ordinary words, and the collision reads as a + * correct classification. */ -export const GROCERY_PATTERN = /coles|woolworths|woolies|aldi|iga|foodworks|costco/i; +export const GROCERY_PATTERN = /\b(coles|woolworths|woolies|aldi|iga|foodworks|costco)\b/i; /** * The normalised merchant used for matching and for the remembered-kind table. @@ -219,10 +226,27 @@ export function toLineItems(items: ExtractedItem[]): StoredLineItem[] { }); } -/** Does the stored merchant name denote the extracted one? Exact key, or one contains the other. */ +/** Is `needle` a contiguous run of whole words inside `hay`? */ +function containsWordRun(hay: string[], needle: string[]): boolean { + if (!needle.length || needle.length > hay.length) return false; + for (let i = 0; i + needle.length <= hay.length; i++) { + if (needle.every((w, j) => hay[i + j] === w)) return true; + } + return false; +} + +/** + * Does the stored merchant name denote the extracted one? One name's words must + * appear as a contiguous run inside the other's — so `Coles` matches + * `Coles (Wyndham Vale)` and `Order - Coles (Wyndham Vale)`, which is spelling, + * not widening. + * + * Whole words, never a raw substring: `merchantKey("Bigals Pizza")` contains + * `iga`, so a pizza shop matched every IGA on the same date and total. + */ export function merchantMatches(stored: string | null | undefined, extracted: string | null | undefined): boolean { - const a = merchantKey(stored); - const b = merchantKey(extracted); - if (!a || !b) return false; - return a === b || a.includes(b) || b.includes(a); + const a = merchantKey(stored).split(" ").filter(Boolean); + const b = merchantKey(extracted).split(" ").filter(Boolean); + if (!a.length || !b.length) return false; + return containsWordRun(a, b) || containsWordRun(b, a); } diff --git a/src/lib/order-ingestion.ts b/src/lib/order-ingestion.ts index e60e4ab..f588477 100644 --- a/src/lib/order-ingestion.ts +++ b/src/lib/order-ingestion.ts @@ -12,13 +12,6 @@ export const CUTOVER_DATE = "2026-01-09"; */ export const DEFAULT_OWNER_ID = 1; -/** Human labels for the platform a receipt came from. */ -export const PLATFORM_LABEL: Record = { - doordash: "DoorDash", - ubereats: "Uber Eats", - uber: "Uber", -}; - /** * Transaction description. *