Three defects on one surface, all invisible while a transaction has exactly
one expense_metadata row — which is almost always.
1. ORDER BY (transaction_id = $1) DESC did the opposite of its comment.
Postgres orders DESC as NULLS FIRST, and the row that merely POINTS at the
transaction has a NULL transaction_id, so the clause written to prefer the
owned row preferred the other one — in exactly the case it exists for.
matchTransaction already spelled out NULLS LAST; now so do this and
GET /api/transactions/[id]/order, which had no ORDER BY at all and could
read a different row from the one an enrichment had just written.
2. replaceLineItems takes an optional metadataId. The match resolves a row
through a platform-filtered join; re-picking independently here can land
the write on a row the match ruled out.
3. previous_line_items on intake_batches is the undo baseline, recorded once
per batch under COALESCE and read back. Returning whatever was just read
hands a pantry retry its own write as the "previous" state, so undo
restores the thing it undid. Migration 0034 is unapplied anywhere, so the
column is added there rather than in a 0035.
One row per upload through POST /api/intake, written BEFORE extraction so a
Slack tap minutes later has something to act on and a re-share (same sha256
set, or the same (platform, merchant, date, total) header already resolved)
is detected. Columns are the §11 C1 contract in smarthome
plans/intake-router-slice-plan.md. intake_merchant_kinds holds the remembered
'Groceries or a meal?' answer per normalised merchant (plan §2, Codex #6).
Not applied to production — finance has no migrate service; apply by hand at
deploy, then npm run test:setup.