From a4e5f540ccc764305e77c88f81329bbd9ed07353 Mon Sep 17 00:00:00 2001 From: siddharthd Date: Fri, 21 Aug 2026 12:10:06 +1000 Subject: [PATCH] docs: intake router routes, env vars, migration 0034 --- CLAUDE.md | 35 +++++++++++++++++++++++++++++++++++ README.md | 14 ++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index ddaace4..216a47a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -622,6 +622,41 @@ and one tap must not flatten it. It says which case it refused on. Slack reaches this route through an n8n webhook, not directly — see the smarthome repo's CLAUDE.md and the `slack-interactive-via-n8n` memory. +### Intake router — one upload, every applicable step (2026-08-21) + +`POST /api/intake` (token `INTAKE_TOKEN`, multipart: `source`, `shared_by`, +`batch_id`, 1–6 `file`s) is the finance half of the share-sheet intake; design +and the cross-repo contract are in smarthome `plans/intake-router-slice-plan.md` +§11 (C1, C2, C5 live here; C3/C4 are pantry-app; C6 is n8n + compose). + +- **Order of operations is the plan's and is load-bearing:** persist + `intake_batches` row → classify each file → ONE LiteLLM call for the whole + batch (`cloud-budget`, `temperature 0`, strict `json_schema`; PDFs go in as + `unpdf` text, never pixels) → header `(merchant, date, total)` → merchant + kind → duplicate check → reconciliation → match → act. `src/lib/intake-rules.ts` + holds the pure decisions, `intake-router.ts` the orchestration. +- **Match is `(platform, merchant, date, total)`, exact, never widened.** A + miss or an ambiguity returns candidates (same merchant ±3 days) with + `intake_use_candidate` / `intake_create` buttons; a transaction is created + only on the explicit tap, ING-10-style (`statement_id NULL`, `owner_id` from + `shared_by` → `participants.email`, else `DEFAULT_OWNER_ID`; + `source='intake'`, `source_ref=batch_id` so a second press is a no-op). +- **Grocery never writes finance here.** The ORIGINAL files go to pantry + `POST /api/capture` with `intake.finance_target_transaction_id` + + `finance_action: enrich|create`; pantry's confirm calls + `PUT /api/transactions/[id]/line-items` back (C4), which is why that route + accepts `RECEIPT_INGEST_TOKEN` too. Files are kept under `INTAKE_FILE_DIR` + keyed by sha256 so a later "Groceries" tap can still forward them. +- **Reconciliation is against the printed item subtotal, not the total.** A + delivery order's total is items + fees − promotions ($118.94 of Coles lines + became $85.74), so checking lines against the total would hold every + delivery order. Tolerance $1 or 2%; a failure is `held`, nothing written. +- **Names stay verbatim.** Burgertory calls its burgers "Michael" and "Joel"; + the prompt forbids canonicalising and the stored shape is the order lane's + (`qty`, `description`, `amount`, `options`) so `order-details.tsx` renders it. +- Statuses beyond the contract list: `ask` (the "Groceries or a meal?" holding + state), `held`, `error` (502 — model or pantry down; the batch row says so). + ### The shared loan The loan is a **separate ledger**, not a shared expense and not a settlement diff --git a/README.md b/README.md index fb38620..c81bbeb 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,18 @@ All routes require authentication via `X-Forwarded-User` header (set by Traefik) | GET / POST | `/api/tags` | List or create tags | | PATCH / DELETE | `/api/tags/[id]` | Update or delete a tag | +**Machine routes** — shared-secret `x-ingest-token` header instead of `X-Forwarded-User`; each fails closed when its token is unset: + +| Method | Route | Token | Description | +|--------|-------|-------|-------------| +| POST / PATCH | `/api/orders/ingest` | `ORDER_INGEST_TOKEN` | n8n order receipts → transactions (see smarthome CLAUDE.md "Order Processor") | +| POST | `/api/receipts/ingest` | `RECEIPT_INGEST_TOKEN` | pantry-app confirmed grocery receipts → one transaction per tender leg (ING-10) | +| POST | `/api/intake` | `INTAKE_TOKEN` | **Intake router** (2026-08-21): multipart `source` (`uber_eats\|doordash\|coles\|woolworths\|other`), `shared_by`, `batch_id` (uuid), 1–6 `file`s (image / PDF / text). Persists `intake_batches`, extracts once via LiteLLM `cloud-budget` (PDF → text via `unpdf`), matches `(platform, merchant, date, total)` exactly, then enriches the matched order's `line_items` (food), forwards the original files to pantry `POST /api/capture` (grocery), or replies with candidates / "Groceries or a meal?" / needs-header / held / duplicate. Returns `{kind, status, transaction_id?, capture_id?, candidates?, slack_blocks, text}` | +| PUT | `/api/transactions/[id]/line-items` | `INTAKE_TOKEN` or `RECEIPT_INGEST_TOKEN` | Idempotent replace of `expense_metadata.line_items` for `transaction_id = :id OR matched_transaction_id = :id`; body `{line_items, source: 'intake'\|'pantry', batch_id?, platform?, replace: true}`; creates the metadata row if absent, never a transaction (404). Returns `{previous_line_items, line_items}` — pantry calls it on every confirm and with `previous_line_items` on undo | +| POST | `/api/slack/interactive` | `ORDER_INGEST_TOKEN` + Slack signature | Order-nudge buttons and the intake taps (`intake_use_candidate`, `intake_create`, `intake_grocery`, `intake_meal`) | + +Env for the intake router: `INTAKE_TOKEN`, `LITELLM_BASE_URL` + `LITELLM_KEY` (OpenAI-compatible, alias `cloud-budget`), `PANTRY_BASE_URL` (+ the existing `RECEIPT_INGEST_TOKEN` as pantry's machine auth), optional `INTAKE_FILE_DIR` (original files by sha256, default `/app/data/intake` — a later "Groceries" tap re-sends them) and `FINANCE_PUBLIC_URL` (the card's View link, default `https://finance.bosecamp.com`). Design: smarthome `plans/intake-router-slice-plan.md` §11. + --- ## Ingestion Pipeline @@ -347,6 +359,8 @@ docker exec postgres-personal psql -U personal -d personal \ | `0009_split_payments` | `split_payments` | | `0010_csv_import_reconcile` | `reconciled_with_id`, CSV import support | | `0011_trips` | `trips`, `trip_id` on `transaction_overrides` (catch-up — was applied directly) | +| … | | +| `0034_intake_batches` | `intake_batches` (one row per `/api/intake` upload, written before extraction) and `intake_merchant_kinds` (the remembered "Groceries or a meal?" answer per merchant) | > `paperless_doc_id` on statements and the `uq_statements_paperless_doc_id` index were added directly (not tracked in a migration file). > `owner_id` on transactions and `statement_id` made nullable were applied directly (March 2026) to support manual transaction entry without a fake statement.