-- ═══════════════════════════════════════════════════════════════════════ -- Nexus Marketplace B2B — Phase 1 schema -- Target: nexus_autoparts (master DB) -- Date: 2026-04-09 -- ═══════════════════════════════════════════════════════════════════════ -- -- This migration is idempotent. Running it multiple times has no effect. -- All new tables use IF NOT EXISTS and ALTERs use IF NOT EXISTS on columns. -- -- Tables: -- 1. bodegas — warehouse registry in the Nexus network -- 2. purchase_orders — PO headers -- 3. purchase_order_items — PO line items -- 4. po_status_history — audit trail for PO state changes -- -- Altered tables: -- - warehouse_inventory (add bodega_id, currency, updated_at) -- -- NOTE: the `users` table alter for marketplace_role + bodega_id lives in -- the TENANT databases (each refaccionaria), not the master DB. That -- migration is applied separately per-tenant via tenant_migrations. -- ═══════════════════════════════════════════════════════════════════════ -- 1. BODEGAS — warehouse registry -- ═══════════════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS bodegas ( id_bodega SERIAL PRIMARY KEY, name VARCHAR(200) NOT NULL, owner_name VARCHAR(200), whatsapp_phone VARCHAR(20) NOT NULL, email VARCHAR(200), city VARCHAR(100), state VARCHAR(50), address TEXT, verified BOOLEAN NOT NULL DEFAULT FALSE, verified_at TIMESTAMP, commission_pct NUMERIC(5, 2) NOT NULL DEFAULT 0, -- reserved for Phase 3 notes TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_bodegas_verified ON bodegas (verified) WHERE verified = TRUE; CREATE INDEX IF NOT EXISTS idx_bodegas_city ON bodegas (city); -- ═══════════════════════════════════════════════════════════════════════ -- 2. WAREHOUSE_INVENTORY — extend existing table -- ═══════════════════════════════════════════════════════════════════════ -- Existing schema has: id_inventory, user_id, part_id, price, stock_quantity, -- min_order_quantity, warehouse_location, updated_at. -- We add bodega_id (FK to the new table) and currency, keeping user_id as -- the legacy owner pointer. ALTER TABLE warehouse_inventory ADD COLUMN IF NOT EXISTS bodega_id INTEGER REFERENCES bodegas(id_bodega), ADD COLUMN IF NOT EXISTS currency VARCHAR(3) NOT NULL DEFAULT 'MXN'; CREATE INDEX IF NOT EXISTS idx_wi_bodega ON warehouse_inventory (bodega_id); -- ═══════════════════════════════════════════════════════════════════════ -- 3. PURCHASE_ORDERS — PO headers -- ═══════════════════════════════════════════════════════════════════════ -- Status state machine: -- draft → submitted → confirmed → ready → delivered → closed -- ↘ rejected (terminal) CREATE TABLE IF NOT EXISTS purchase_orders ( id_po SERIAL PRIMARY KEY, buyer_tenant_id INTEGER NOT NULL, buyer_user_id INTEGER NOT NULL, buyer_phone VARCHAR(20), buyer_email VARCHAR(200), buyer_display_name VARCHAR(200), bodega_id INTEGER NOT NULL REFERENCES bodegas(id_bodega), status VARCHAR(20) NOT NULL DEFAULT 'draft', total_amount NUMERIC(12, 2), currency VARCHAR(3) NOT NULL DEFAULT 'MXN', buyer_notes TEXT, seller_notes TEXT, delivery_method VARCHAR(50), -- 'pickup' | 'delivery' delivery_address TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW(), submitted_at TIMESTAMP, confirmed_at TIMESTAMP, ready_at TIMESTAMP, delivered_at TIMESTAMP, closed_at TIMESTAMP, CONSTRAINT chk_po_status CHECK ( status IN ('draft', 'submitted', 'confirmed', 'rejected', 'ready', 'delivered', 'closed') ) ); CREATE INDEX IF NOT EXISTS idx_po_buyer ON purchase_orders (buyer_tenant_id, buyer_user_id); CREATE INDEX IF NOT EXISTS idx_po_bodega ON purchase_orders (bodega_id, status); CREATE INDEX IF NOT EXISTS idx_po_status ON purchase_orders (status); CREATE INDEX IF NOT EXISTS idx_po_created ON purchase_orders (created_at DESC); -- ═══════════════════════════════════════════════════════════════════════ -- 4. PURCHASE_ORDER_ITEMS — line items -- ═══════════════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS purchase_order_items ( id_po_item SERIAL PRIMARY KEY, po_id INTEGER NOT NULL REFERENCES purchase_orders(id_po) ON DELETE CASCADE, part_id INTEGER NOT NULL REFERENCES parts(id_part), oem_part_number VARCHAR(100), part_name VARCHAR(300), manufacturer VARCHAR(200), quantity INTEGER NOT NULL CHECK (quantity > 0), unit_price NUMERIC(12, 2), subtotal NUMERIC(12, 2), confirmed_qty INTEGER, -- bodega may adjust after confirmation notes TEXT ); CREATE INDEX IF NOT EXISTS idx_po_items_po ON purchase_order_items (po_id); CREATE INDEX IF NOT EXISTS idx_po_items_part ON purchase_order_items (part_id); -- ═══════════════════════════════════════════════════════════════════════ -- 5. PO_STATUS_HISTORY — audit trail -- ═══════════════════════════════════════════════════════════════════════ CREATE TABLE IF NOT EXISTS po_status_history ( id_history SERIAL PRIMARY KEY, po_id INTEGER NOT NULL REFERENCES purchase_orders(id_po) ON DELETE CASCADE, from_status VARCHAR(20), to_status VARCHAR(20) NOT NULL, actor_user_id INTEGER, actor_kind VARCHAR(20), -- 'buyer' | 'seller' | 'system' | 'admin' note TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_po_history_po ON po_status_history (po_id, created_at); -- ═══════════════════════════════════════════════════════════════════════ -- Verification queries -- ═══════════════════════════════════════════════════════════════════════ -- Run after applying to check everything landed: -- SELECT COUNT(*) FROM bodegas; -- SELECT COUNT(*) FROM purchase_orders; -- SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'warehouse_inventory' AND column_name IN ('bodega_id', 'currency');