RECORDGOD/schema.sql
type-two e1fcf94f93 feat: standalone RecordGod web UI — landing, control-room dash, encrypted secrets vault
- app/vault.py: Fernet-encrypted credential store (app_secret table); key lives only in
  RECORDGOD_SECRET_KEY (.env, gitignored) — refuses to store anything if the key is unset.
- app/settings_routes.py: admin-gated GET/POST /settings/secrets; values encrypted at rest,
  NEVER returned. Known fields: Woo / Discogs / Cloudflare / DealGod credentials.
- site/index.html: recordgod.com landing. site/dash.html: control room (token gate +
  paste-your-credentials form). main.py loads .env, mounts site at /, store at /store.
- Proven: 401 on bad token; DB holds ciphertext (gAAAA…), not plaintext.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:40:07 +10:00

99 lines
4.9 KiB
SQL

-- RecordGod schema v2.
-- ONE inventory table (vinyl + general goods unified), sales history, slim crates.
-- The Discogs CATALOG is deliberately NOT stored here — it lives in the discogs_full Postgres
-- mirror and is joined on release_id at read time. We keep the shop's OWN data only:
-- WowPlatter crammed a ~3.2M-row Discogs clone into the shop DB; RecordGod keeps ~40k real rows.
CREATE TABLE IF NOT EXISTS inventory (
sku text PRIMARY KEY,
store_id int NOT NULL DEFAULT 1, -- always 1 for now; the multi-shop seam
kind text NOT NULL DEFAULT 'vinyl', -- vinyl|book|magazine|comic|card|game|other
release_id bigint, -- Discogs id (vinyl) → join discogs_full.release
identifier text, -- ISBN/UPC/barcode (general goods lookup key)
title text, -- denormalised cache; vinyl filled from mirror at intake
price numeric(10,2),
qty int NOT NULL DEFAULT 1,
in_stock boolean NOT NULL DEFAULT true, -- was enum('y','n')
condition text, -- media grade (vinyl) / condition desc (general)
sleeve_cond text, -- vinyl sleeve grade
brand text,
model text,
weight_g numeric(10,3),
location text,
crate_id int,
slot_number int,
status text DEFAULT 'publish',
notes text,
product_url text,
-- DealGod price intel (the engine writes these)
est_market_value numeric(10,2),
lowest_competitor numeric(10,2),
target_price numeric(10,2),
priced_at timestamptz,
-- Square sync (Square/WP authoritative)
square_item_id text,
square_catalog_object_id text,
square_sku text,
square_synced_at timestamptz,
-- intake / staging (live owns sale-state; local owns intake — see RECORDGOD_PLAN.md §6)
staged boolean NOT NULL DEFAULT false, -- true = intaken locally, not yet published live
attributes jsonb, -- per-kind extras (dimensions, etc.) — was longtext+CHECK
images jsonb,
raw_json jsonb, -- enrichment cache
sold_date timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS inventory_release_idx ON inventory (store_id, release_id) WHERE release_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS inventory_identifier_idx ON inventory (store_id, identifier) WHERE identifier IS NOT NULL;
CREATE INDEX IF NOT EXISTS inventory_instock_idx ON inventory (store_id, in_stock) WHERE in_stock;
CREATE INDEX IF NOT EXISTS inventory_crate_idx ON inventory (crate_id, slot_number);
CREATE INDEX IF NOT EXISTS inventory_staged_idx ON inventory (staged) WHERE staged;
CREATE TABLE IF NOT EXISTS crate (
id int PRIMARY KEY,
name text NOT NULL,
label_text text,
purpose text DEFAULT 'stock', -- stock|spacer|display|returns|hold|sale
notes text
-- 3D geometry (pos/rotation/mesh/genre-lists) intentionally stays in WowPlatter — front-end concern.
);
CREATE TABLE IF NOT EXISTS sales (
id bigint PRIMARY KEY,
sale_number text UNIQUE,
customer_id bigint,
total numeric(10,2) NOT NULL DEFAULT 0, -- consolidated (WP had total + total_amount)
tax_amount numeric(10,2) NOT NULL DEFAULT 0,
status text DEFAULT 'pending', -- consolidated (WP had status + payment_status)
payment_method text,
sale_date timestamptz,
created_at timestamptz
-- WooCommerce/WP stays authoritative for live sale-state; this is the reporting/analytics copy.
);
CREATE INDEX IF NOT EXISTS sales_date_idx ON sales (sale_date);
CREATE TABLE IF NOT EXISTS sale_items (
id bigint PRIMARY KEY,
sale_id bigint REFERENCES sales(id) ON DELETE CASCADE,
sku text,
release_id bigint,
item_name text,
qty int NOT NULL DEFAULT 1,
unit_price numeric(10,2) NOT NULL DEFAULT 0,
line_total numeric(10,2) NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS sale_items_sale_idx ON sale_items (sale_id);
CREATE INDEX IF NOT EXISTS sale_items_release_idx ON sale_items (release_id);
-- Encrypted credential vault. Values are Fernet ciphertext; the key lives in the environment
-- (RECORDGOD_SECRET_KEY), never here. Woo/Discogs/Cloudflare/DealGod tokens land here via /settings.
CREATE TABLE IF NOT EXISTS app_secret (
name text PRIMARY KEY,
value_enc bytea NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
-- pgvector (semantic search / "records like this" / newsletter copy): add when keyword search
-- measurably falls short, not before.