intake: match merchants on whole words, not substrings

merchantKey("Bigals Pizza") contains "iga", so GROCERY_PATTERN filed a pizza
shop as a grocer and merchantMatches matched it against every IGA on the same
date and total. Short names in a substring list collide with ordinary words,
and the collision reads as a correct classification rather than a bug.

GROCERY_PATTERN is now a word-boundary alternation; merchantMatches asks
whether one name's words appear as a contiguous run inside the other's, which
keeps Coles <-> Coles (Wyndham Vale) and Order - Coles (Wyndham Vale).

Also collapses the third PLATFORM_LABEL copy: order-ingestion's was exported
and read nowhere, order-details.tsx had its own, and the intake one — the only
live copy — was missing `uber`. One map, in intake-rules.
This commit is contained in:
2026-08-21 17:52:25 +10:00
parent a4e5f540cc
commit be3d10d4f1
3 changed files with 36 additions and 22 deletions
+4 -7
View File
@@ -13,12 +13,9 @@ import {
type OrderReceipt,
type OrderRating,
} from "@/lib/hooks";
const PLATFORM_LABEL: Record<string, string> = {
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({
<p className="text-xs text-zinc-500">
Order details
{receipt.platform && (
<span className="ml-1.5 text-zinc-400">{PLATFORM_LABEL[receipt.platform] ?? receipt.platform}</span>
<span className="ml-1.5 text-zinc-400">{platformLabel(receipt.platform)}</span>
)}
</p>
{receipt.card_last4 && (