db(0034): intake_batches + intake_merchant_kinds for the intake router
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.
This commit is contained in:
@@ -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.';
|
||||||
@@ -268,3 +268,28 @@ model rule_apply_runs {
|
|||||||
reverted_at DateTime?
|
reverted_at DateTime?
|
||||||
snapshot Json @default("[]")
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user