- 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>
99 lines
4.9 KiB
SQL
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.
|