diff --git a/CLAUDE.md b/CLAUDE.md index 7da2365..18d6e48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -183,8 +183,45 @@ category-level ratio that "looks wrong" usually is not. offset account, 39 rows, $37,980.24) are categorised `transfers`, indistinguishable from ordinary internal transfers. The loan model below is unbuilt. -**`order_reviews` is a table wired to nothing** — 0 rows, no API, no UI, no -writes. The "never order from here again" capability does not exist. +### Order verdicts — "never order from here again" + +Built 2026-07-28 (migration 0024). `order_reviews` was previously a table wired +to nothing; it now backs `GET`/`PUT /api/transactions/[id]/review`, with the UI +inside `components/order-details.tsx`. Logic in `lib/order-reviews.ts`. + +Four things about the shape, each of which is load-bearing: + +- **Per person, not per order.** `UNIQUE (transaction_id, participant_id)`. A + shared meal produces two opinions that routinely disagree, and the + disagreement is the useful part. `PUT` defaults to the **signed-in user**, + not the owner — Sonu authenticates through the same Traefik OAuth as + participant 4, so an owner default would file her verdict under his name. +- **Four levels** — `loved`, `liked`, `ok`, `never`. Three collapsed the + distinction that decides a re-order: "loved" and "liked" both mean "would + order again" but only one is worth a detour, and "ok" is not a + recommendation. +- **Item verdicts key on the item DESCRIPTION**, not its index — an index is + meaningless across orders, and "the Pad Thai here is good" has to survive + into the next order from the same merchant. Pooled case-folded across the + merchant's orders. Only `loved`/`never`: a per-item "ok" answers neither + question you ask at order time. +- **An ABSENT `item_verdicts` means "leave them alone"; `[]` clears them.** + Without that distinction a note-only save wipes every per-item opinion — the + same shape as the bug that reset `settled` on split rewrites, and just as + invisible on screen. + +**Sharing is a real 50/50 split, not a flag.** The "Shared with Sonu" toggle +writes `transaction_splits`, because the split already IS the record that an +order was shared and two records of one fact drift apart. Safe to clear on an +order because an ingested order is post-cutover by construction. + +The merchant signal is *derived* by aggregating a merchant's orders on +`expense_metadata.merchant_normalized` — never `transactions.merchant_name`, +which is a bank descriptor and reads differently for the same restaurant on +different nights. + +`/api/orders/ingest` returns `prior_verdict` so the n8n Slack nudge can warn at +the moment an order lands. ### The shared loan