diff --git a/backend/migrations/1787500000000_add-intake-pipeline.js b/backend/migrations/1787500000000_add-intake-pipeline.js new file mode 100644 index 0000000..407a236 --- /dev/null +++ b/backend/migrations/1787500000000_add-intake-pipeline.js @@ -0,0 +1,80 @@ +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE IF NOT EXISTS upload_links ( + id SERIAL PRIMARY KEY, + label TEXT NOT NULL, + -- The token itself is never stored, only its digest. A leaked database + -- is then not also a leaked set of working upload links, and the admin + -- screen can show a token exactly once — at creation — for the same + -- reason a password reset link is not re-readable. + token_hash TEXT NOT NULL UNIQUE, + revoked_at TIMESTAMPTZ, + submission_count INTEGER NOT NULL DEFAULT 0, + -- Null means no cap. A link handed to a regular contributor is + -- open-ended; one handed out for a single box of stock is not. The + -- route defaults this to a finite number rather than null, so an + -- unbounded link is something asked for rather than something that + -- happens when nobody thought about it. + max_submissions INTEGER, + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + + CREATE TABLE IF NOT EXISTS item_drafts ( + id SERIAL PRIMARY KEY, + item_id INTEGER NOT NULL UNIQUE REFERENCES items(id) ON DELETE CASCADE, + -- SET NULL rather than CASCADE: deleting a link must not delete the + -- items that arrived through it. Provenance is lost; the goods are not. + upload_link_id INTEGER REFERENCES upload_links(id) ON DELETE SET NULL, + submitter_note TEXT, + state TEXT NOT NULL DEFAULT 'queued', + attempts INTEGER NOT NULL DEFAULT 0, + model TEXT, + ai_name TEXT, + ai_description TEXT, + ai_category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL, + ai_tag_names TEXT[], + -- Kept even though a suggestion is also copied onto the item, so what + -- the model proposed stays readable after the admin has edited the + -- item's price. Without it there is no way to ask later whether the + -- model's numbers were any good. + ai_suggested_price_cents INTEGER, + -- ai | default | admin. Where the item's current price came from. + -- Recorded rather than inferred: a model that happens to suggest exactly + -- 8000, or an admin who deliberately types the model's number, both + -- collapse any comparison-based guess. + price_source TEXT NOT NULL DEFAULT 'default', + ai_error TEXT, + input_tokens INTEGER, + output_tokens INTEGER, + cost_micros INTEGER, + drafted_at TIMESTAMPTZ, + reviewed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + + -- The review queue reads by state; everything else reads by item, which + -- the UNIQUE constraint on item_id already indexes. + CREATE INDEX IF NOT EXISTS item_drafts_state_idx ON item_drafts (state); + + -- A submitted item is priced on arrival rather than left unpriced, so the + -- column keeps NOT NULL and only gains a fallback. 80.00 applies when + -- nothing else supplies a price; the drafting worker in #223 writes a + -- model's suggestion over it when there is one. + -- + -- The number lives here rather than in configuration deliberately. + -- Changing a default price is a rare, deliberate act that deserves a + -- record; an environment variable would let it drift silently between + -- environments, and a wrong default is invisible until something has + -- already sold at it. + ALTER TABLE items ALTER COLUMN price_cents SET DEFAULT 8000; + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + ALTER TABLE items ALTER COLUMN price_cents DROP DEFAULT; + DROP TABLE IF EXISTS item_drafts; + DROP TABLE IF EXISTS upload_links; + `); +}; diff --git a/backend/tests/integration/setup/testDb.ts b/backend/tests/integration/setup/testDb.ts index 2f6280c..e5b191a 100755 --- a/backend/tests/integration/setup/testDb.ts +++ b/backend/tests/integration/setup/testDb.ts @@ -28,9 +28,9 @@ export async function migrate(): Promise { export async function resetDb(): Promise { await testPool.query(` - TRUNCATE TABLE orders, checkout_items, checkouts, shipping_addresses, cart_items, carts, - customer_tokens, customer_sessions, favorites, customers, item_tags, item_images, items, - tags, categories + TRUNCATE TABLE item_drafts, upload_links, orders, checkout_items, checkouts, + shipping_addresses, cart_items, carts, customer_tokens, customer_sessions, favorites, + customers, item_tags, item_images, items, tags, categories RESTART IDENTITY CASCADE `);