-- ============================================ -- Gunbuilder / AvantLink Schema DDL -- ============================================ -- Enable extension for UUID generation (if not already enabled) CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ============================================ -- 1. merchants -- ============================================ CREATE TABLE merchants ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, avantlink_mid TEXT NOT NULL UNIQUE, -- AvantLink merchant ID feed_url TEXT NOT NULL, -- Product datafeed URL is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_merchants_active ON merchants (is_active); -- ============================================ -- 2. part_categories -- Your internal Gunbuilder categories -- ============================================ CREATE TABLE part_categories ( id SERIAL PRIMARY KEY, slug TEXT NOT NULL UNIQUE, -- 'lower', 'upper', 'barrel', etc. name TEXT NOT NULL, -- 'Lower Receiver' description TEXT ); -- ============================================ -- 3. brands -- ============================================ CREATE TABLE brands ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, -- 'Aero Precision' website TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- 4. products -- Canonical parts, independent of merchants -- ============================================ CREATE TABLE products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), brand_id INTEGER REFERENCES brands(id), part_category_id INTEGER NOT NULL REFERENCES part_categories(id), name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT, caliber TEXT, barrel_length_mm INTEGER, gas_system TEXT, handguard_length_mm INTEGER, image_url TEXT, spec_sheet_url TEXT, msrp NUMERIC(10,2), current_lowest_price NUMERIC(10,2), current_lowest_merchant_id INTEGER REFERENCES merchants(id), is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_products_category ON products (part_category_id); CREATE INDEX idx_products_brand ON products (brand_id); CREATE INDEX idx_products_active ON products (is_active); CREATE INDEX idx_products_lowest_price ON products (current_lowest_price); -- Optional: a "natural-ish" uniqueness constraint if you want it -- (Can be relaxed later if needed) CREATE UNIQUE INDEX uniq_products_brand_name_cat ON products (brand_id, name, part_category_id); -- ============================================ -- 5. product_offers -- Merchant-specific offers for a product -- ============================================ CREATE TABLE product_offers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE, merchant_id INTEGER NOT NULL REFERENCES merchants(id), avantlink_product_id TEXT NOT NULL, sku TEXT, upc TEXT, buy_url TEXT NOT NULL, price NUMERIC(10,2) NOT NULL, original_price NUMERIC(10,2), currency TEXT NOT NULL DEFAULT 'USD', in_stock BOOLEAN NOT NULL DEFAULT TRUE, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Each merchant + avantlink_product_id combination must be unique CREATE UNIQUE INDEX uniq_product_offers_merchant_product ON product_offers (merchant_id, avantlink_product_id); CREATE INDEX idx_product_offers_product ON product_offers (product_id); CREATE INDEX idx_product_offers_merchant ON product_offers (merchant_id); CREATE INDEX idx_product_offers_in_stock ON product_offers (in_stock); CREATE INDEX idx_product_offers_price ON product_offers (price); -- ============================================ -- 6. price_history -- For tracking price changes over time -- ============================================ CREATE TABLE price_history ( id BIGSERIAL PRIMARY KEY, product_offer_id UUID NOT NULL REFERENCES product_offers(id) ON DELETE CASCADE, price NUMERIC(10,2) NOT NULL, recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_price_history_offer ON price_history (product_offer_id); CREATE INDEX idx_price_history_recorded_at ON price_history (recorded_at); -- ============================================ -- 7. category_mappings -- Map raw AvantLink categories to internal part_categories -- ============================================ CREATE TABLE category_mappings ( id SERIAL PRIMARY KEY, merchant_id INTEGER NOT NULL REFERENCES merchants(id), raw_category_path TEXT NOT NULL, -- e.g. 'Firearms > AR-15 Parts > Lower Receivers' part_category_id INTEGER NOT NULL REFERENCES part_categories(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (merchant_id, raw_category_path) ); CREATE INDEX idx_category_mappings_merchant ON category_mappings (merchant_id); CREATE INDEX idx_category_mappings_part_category ON category_mappings (part_category_id); -- ============================================ -- 8. feed_imports -- Track each feed import run for observability -- ============================================ CREATE TABLE feed_imports ( id BIGSERIAL PRIMARY KEY, merchant_id INTEGER NOT NULL REFERENCES merchants(id), started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), finished_at TIMESTAMPTZ, rows_total INTEGER, rows_processed INTEGER, rows_new INTEGER, rows_updated INTEGER, status TEXT NOT NULL DEFAULT 'running', -- 'running','success','error' error_message TEXT ); CREATE INDEX idx_feed_imports_merchant ON feed_imports (merchant_id); CREATE INDEX idx_feed_imports_status ON feed_imports (status); CREATE INDEX idx_feed_imports_started_at ON feed_imports (started_at); -- ============================================ -- 9. Helpful seed data (optional) -- ============================================ -- Part categories seed (adjust as needed) INSERT INTO part_categories (slug, name) VALUES ('lower', 'Lower Receiver'), ('upper', 'Upper Receiver'), ('barrel', 'Barrel'), ('handguard', 'Handguard'), ('stock', 'Stock'), ('optic', 'Optic') ON CONFLICT (slug) DO NOTHING; -- Example merchant skeleton (fill real datafeed URL + AvantLink MID) -- INSERT INTO merchants (name, avantlink_mid, feed_url) -- VALUES ('Example Merchant', '12345', 'https://www.avantlink.com/api/...');