This commit is contained in:
@@ -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
|
Slack reaches this route through an n8n webhook, not directly — see the
|
||||||
smarthome repo's CLAUDE.md and the `slack-interactive-via-n8n` memory.
|
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 shared loan
|
||||||
|
|
||||||
The loan is a **separate ledger**, not a shared expense and not a settlement
|
The loan is a **separate ledger**, not a shared expense and not a settlement
|
||||||
|
|||||||
@@ -282,6 +282,18 @@ All routes require authentication via `X-Forwarded-User` header (set by Traefik)
|
|||||||
| GET / POST | `/api/tags` | List or create tags |
|
| GET / POST | `/api/tags` | List or create tags |
|
||||||
| PATCH / DELETE | `/api/tags/[id]` | Update or delete a tag |
|
| 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
|
## Ingestion Pipeline
|
||||||
@@ -347,6 +359,8 @@ docker exec postgres-personal psql -U personal -d personal \
|
|||||||
| `0009_split_payments` | `split_payments` |
|
| `0009_split_payments` | `split_payments` |
|
||||||
| `0010_csv_import_reconcile` | `reconciled_with_id`, CSV import support |
|
| `0010_csv_import_reconcile` | `reconciled_with_id`, CSV import support |
|
||||||
| `0011_trips` | `trips`, `trip_id` on `transaction_overrides` (catch-up — was applied directly) |
|
| `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).
|
> `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.
|
> `owner_id` on transactions and `statement_id` made nullable were applied directly (March 2026) to support manual transaction entry without a fake statement.
|
||||||
|
|||||||
Reference in New Issue
Block a user