From d75589e128f6b700a531ec613edfb307328f232e Mon Sep 17 00:00:00 2001 From: siddharthd Date: Sat, 3 Oct 2026 17:30:48 +1000 Subject: [PATCH] docs: the wallet lane -- storage convention, pairing, lock, skips, replay Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index bed5e82..5eafb26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -325,6 +325,42 @@ paid 2024/2025 as `VICROADS` and 2026 as `VICROADS ONLINE PAYM`, so the series splits and the 2025 estimate shows as `expected_not_seen`. Not fuzzy-matched on purpose — see the rejected pg_trgm merchant grouping. +### The wallet lane (`/api/wallet/ingest`, `lib/wallet-ingestion.ts`) + +iOS notifications → `capture.wallet_events` → n8n **Wallet Interpret** +(`CAp1s5cuzJk2mDaI`, local gemma, every 15 min) → this route. A row per tap, +statementless, `source='wallet'`, `source_ref='evt:'`; the statement later +confirms it through the reconcile queue. Fixed 2026-10-03 after a month abroad +(ticket #266); the rules below are each the fix for something that broke. + +- **Storage convention differs from the order lane.** `amount` = `amount_aud` = + a *provisional AUD* figure; the native figure is `foreign_currency_amount`. + `NATIVE_CURRENCY` reads `amount_aud IS NOT NULL` as "amount is AUD" — without + that an INR 524 tap displayed as "INR 9.42". +- **Provisional rate:** ledger median over *statement* rows (≥5, carries the + card's FX fee) → else ECB via Frankfurter (`rate_ecb`) → else `0` with + `no_rate_reference`. Never the native figure as AUD (672006 VND once became + A$672,006). +- **One tap = two notifications** (issuer app + Wallet, 2–8 s apart), merged. + A pair must be **≤10 minutes apart AND from different apps** + (`expense_metadata.notified_at` / `notif_app`, migration 0039). The old + amount-within-±1-day rule merged the UNICEF $50 into an unrelated $50 charge. +- **The whole check→pair→adopt→insert runs under one advisory lock** + (`hashtextextended('wallet-ingest',0)`), on the transaction's own connection. + n8n posts a poll's items concurrently; without it 11 taps became 22 rows. + Prisma cannot deserialise `void` — the lock SELECT needs `::text`. +- **A merged half records `wallet_evt:` in the survivor's flags**, so a + replay is idempotent. Reconciled wallet rows carry **no** `expense_metadata` + (reconcile moves it to the statement twin) — the flag check follows it there. +- **Skipped, not booked:** account notices (`overdrawn`, `to avoid fees`, …) and + transit-card drawdowns (`Your new/current balance is` — the Octopus *load* is + the spend). The fixed-width descriptor parser runs on Westpac bodies only. +- **Dated in Australia/Melbourne**, like the statements (UTC slicing filed 28 of + 134 rows a day early). +- **Replay a few events by relabelling their interpretations** + (`UPDATE capture.wallet_interpretations SET prompt_version='v1-' WHERE …`), + not by bumping `prompt_version`, which re-posts every event ever captured. + ### Cash and reconciliation `payment_method` in (`cash`, `credits`) excludes a transaction from