diff --git a/prisma/migrations/0034_intake_batches/migration.sql b/prisma/migrations/0034_intake_batches/migration.sql new file mode 100644 index 0000000..3fc9375 --- /dev/null +++ b/prisma/migrations/0034_intake_batches/migration.sql @@ -0,0 +1,64 @@ +-- Intake router (plans/intake-router-slice-plan.md §11 C1, smarthome repo). +-- +-- One upload point for receipts — a screenshot, a PDF export, a pasted body — +-- that reads what it was given and does every step that applies: fill the +-- empty `line_items` on the order the email pipeline already booked, hand a +-- grocery receipt to pantry, or ask when it cannot tell. +-- +-- Every batch is persisted BEFORE anything is extracted, for two reasons: +-- 1. The Slack card it produces carries buttons ("Use this", "Create", +-- "Groceries or a meal?") that are pressed minutes later in a fresh +-- request with nothing but the batch id to go on. +-- 2. A re-share of an already-handled order — a second crop, the PDF after +-- the screenshot — must be recognised, and the only durable evidence of +-- the first share is this row (`file_sha256` overlap, or the same +-- (platform, merchant, date, total) header already resolved). +-- +-- `id` is the uuid n8n mints per webhook call, so a retried delivery is the +-- same batch rather than a second one. `header` holds the extracted +-- (merchant, order_date, total, platform, ...) and `items` the verbatim item +-- list, so a later tap can enrich or create without re-reading the files. +-- +-- Idempotent: safe to re-run. + +CREATE TABLE IF NOT EXISTS intake_batches ( + id uuid PRIMARY KEY, + source text NOT NULL, + shared_by text, + file_sha256 text[] NOT NULL DEFAULT '{}', + header jsonb, + items jsonb, + kind text, + status text NOT NULL, + transaction_id integer, + capture_id integer, + created_at timestamptz NOT NULL DEFAULT now(), + resolved_at timestamptz +); + +COMMENT ON TABLE intake_batches IS + 'One row per upload batch through POST /api/intake. Persisted before ' + 'extraction so Slack taps can act on it later and re-shares are detected. ' + 'kind: grocery|food|ask|held|unsupported. status: received|enriched|created|' + 'candidates|forwarded_to_pantry|needs_header|duplicate|unsupported|held|error.'; + +-- Overlap lookups for the re-share check (`file_sha256 && $1`). +CREATE INDEX IF NOT EXISTS intake_batches_file_sha256_gin + ON intake_batches USING gin (file_sha256); + +CREATE INDEX IF NOT EXISTS intake_batches_transaction_id + ON intake_batches (transaction_id) WHERE transaction_id IS NOT NULL; + +-- The "Groceries or a meal?" answer, remembered per merchant (plan §2, Codex +-- #6). A tiny answer table, not merchant master-data: the pattern list in code +-- decides the obvious cases and this catches the `Romeo's Food Hall`s. Keyed on +-- the normalised merchant name the router already computes for matching. +CREATE TABLE IF NOT EXISTS intake_merchant_kinds ( + merchant_key text PRIMARY KEY, + kind text NOT NULL CHECK (kind IN ('grocery', 'food')), + created_at timestamptz NOT NULL DEFAULT now() +); + +COMMENT ON TABLE intake_merchant_kinds IS + 'Per-merchant answer to "Groceries or a meal?" from the intake Slack card. ' + 'Consulted after the grocery pattern list; an unknown merchant asks once.'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c06757a..cdf021c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -268,3 +268,28 @@ model rule_apply_runs { reverted_at DateTime? snapshot Json @default("[]") } + +// 0034 — intake router. One row per upload batch, written before extraction so +// Slack taps can act on it later and a re-share is detected. Columns are the +// §11 C1 contract in plans/intake-router-slice-plan.md (smarthome repo). +model intake_batches { + id String @id @db.Uuid + source String + shared_by String? + file_sha256 String[] @default([]) + header Json? + items Json? + kind String? + status String + transaction_id Int? + capture_id Int? + created_at DateTime @default(now()) @db.Timestamptz(6) + resolved_at DateTime? @db.Timestamptz(6) +} + +// 0034 — the remembered answer to "Groceries or a meal?" per merchant. +model intake_merchant_kinds { + merchant_key String @id + kind String + created_at DateTime @default(now()) @db.Timestamptz(6) +}