RECORDGOD/schema.sql

149 lines
6.8 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);
-- Cached Discogs catalog for enrichment — RecordGod's OWN copy of just the releases it stocks,
-- so the store is self-contained: no live dependency on the full Discogs dump OR dealgod's DB.
-- Populated by build_disc_cache.py from the full mirror on ultra.
CREATE TABLE IF NOT EXISTS disc_cache (
release_id bigint PRIMARY KEY,
title text,
artist text,
thumb text,
weight int
);
-- 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()
);
-- POS customers (from wp_rmp_disc_sales_customers) — the counter's customer dropdown + add-new.
CREATE TABLE IF NOT EXISTS customer (
id bigint PRIMARY KEY,
wp_user_id bigint,
first_name text NOT NULL DEFAULT '',
last_name text DEFAULT '',
email text,
phone text,
address text,
is_guest boolean NOT NULL DEFAULT false,
created_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS customer_name_idx ON customer (lower(first_name), lower(last_name));
CREATE INDEX IF NOT EXISTS customer_email_idx ON customer (lower(email));
-- Named discounts / promotions (from wp_rmp_disc_sales_discounts) — the Promotions tab + POS promo picker.
CREATE TABLE IF NOT EXISTS sales_discount (
id bigint PRIMARY KEY,
name text NOT NULL,
percentage numeric(5,2) NOT NULL DEFAULT 0,
is_active boolean NOT NULL DEFAULT true,
manual_active boolean NOT NULL DEFAULT false, -- currently switched on at the counter
discount_type text DEFAULT 'manual', -- manual|promotion|tax
description text,
bubble_text text,
bubble_color text,
start_at timestamptz,
end_at timestamptz,
created_at timestamptz DEFAULT now()
);
-- Sales settings (from wp_rmp_disc_sales_settings) — currency / tax / default discount, key-value.
CREATE TABLE IF NOT EXISTS sales_setting (
setting_key text PRIMARY KEY,
setting_value text,
is_active boolean NOT NULL DEFAULT true,
updated_at timestamptz DEFAULT now()
);
-- pgvector (semantic search / "records like this" / newsletter copy): add when keyword search
-- measurably falls short, not before.