-- BOC Schema Extension: Quotes, Orders, Suppliers, Inventory -- Created: 2026-07-12 -- ============================================ -- SALES: Quotes (Offert) -- ============================================ CREATE TABLE IF NOT EXISTS boc_quotes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE, contact_id UUID REFERENCES boc_contacts(id), quote_number TEXT NOT NULL, title TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, accepted, rejected, expired amount DECIMAL(15,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(15,2) DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', valid_until DATE, accepted_at TIMESTAMPTZ, converted_to_order_id UUID, notes TEXT, terms TEXT, metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_quote_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, quote_id UUID REFERENCES boc_quotes(id) ON DELETE CASCADE, product_id UUID REFERENCES boc_products(id), description TEXT NOT NULL, quantity DECIMAL(10,2) NOT NULL DEFAULT 1, unit_price DECIMAL(15,2) NOT NULL DEFAULT 0, tax_rate DECIMAL(5,2) DEFAULT 0, discount DECIMAL(5,2) DEFAULT 0, total DECIMAL(15,2) NOT NULL DEFAULT 0, sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- SALES: Orders -- ============================================ CREATE TABLE IF NOT EXISTS boc_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE, quote_id UUID REFERENCES boc_quotes(id), order_number TEXT NOT NULL, title TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', -- draft, confirmed, processing, shipped, delivered, cancelled amount DECIMAL(15,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(15,2) DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', delivery_date DATE, shipped_at TIMESTAMPTZ, delivered_at TIMESTAMPTZ, tracking_number TEXT, notes TEXT, metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_order_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, order_id UUID REFERENCES boc_orders(id) ON DELETE CASCADE, product_id UUID REFERENCES boc_products(id), description TEXT NOT NULL, quantity DECIMAL(10,2) NOT NULL DEFAULT 1, unit_price DECIMAL(15,2) NOT NULL DEFAULT 0, tax_rate DECIMAL(5,2) DEFAULT 0, discount DECIMAL(5,2) DEFAULT 0, total DECIMAL(15,2) NOT NULL DEFAULT 0, delivered_qty DECIMAL(10,2) DEFAULT 0, sort_order INTEGER DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- PURCHASE: Suppliers -- ============================================ CREATE TABLE IF NOT EXISTS boc_suppliers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, email TEXT, phone TEXT, org_number TEXT, address JSONB, payment_terms TEXT DEFAULT '30 days', bank_account TEXT, bankgiro TEXT, postgiro TEXT, currency TEXT DEFAULT 'USD', status TEXT NOT NULL DEFAULT 'active', metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- PURCHASE: Purchase Orders -- ============================================ CREATE TABLE IF NOT EXISTS boc_purchase_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, supplier_id UUID REFERENCES boc_suppliers(id) ON DELETE CASCADE, po_number TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'draft', -- draft, sent, confirmed, received, invoiced, paid amount DECIMAL(15,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(15,2) DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', expected_delivery DATE, received_at TIMESTAMPTZ, notes TEXT, metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_purchase_order_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, po_id UUID REFERENCES boc_purchase_orders(id) ON DELETE CASCADE, product_id UUID REFERENCES boc_products(id), description TEXT NOT NULL, quantity DECIMAL(10,2) NOT NULL DEFAULT 1, unit_price DECIMAL(15,2) NOT NULL DEFAULT 0, tax_rate DECIMAL(5,2) DEFAULT 0, total DECIMAL(15,2) NOT NULL DEFAULT 0, received_qty DECIMAL(10,2) DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- PURCHASE: Supplier Invoices (Leverantörsfakturor) -- ============================================ CREATE TABLE IF NOT EXISTS boc_supplier_invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, supplier_id UUID REFERENCES boc_suppliers(id), po_id UUID REFERENCES boc_purchase_orders(id), invoice_number TEXT NOT NULL, amount DECIMAL(15,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(15,2) DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', status TEXT NOT NULL DEFAULT 'draft', -- draft, received, approved, paid, disputed due_date DATE, paid_at TIMESTAMPTZ, paid_amount DECIMAL(15,2) DEFAULT 0, ocr_number TEXT, notes TEXT, metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- INVENTORY: Stock / Warehouse -- ============================================ CREATE TABLE IF NOT EXISTS boc_warehouses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, location TEXT, address JSONB, is_default BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_inventory ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, product_id UUID REFERENCES boc_products(id) ON DELETE CASCADE, warehouse_id UUID REFERENCES boc_warehouses(id), quantity DECIMAL(10,2) NOT NULL DEFAULT 0, reserved_qty DECIMAL(10,2) DEFAULT 0, reorder_point DECIMAL(10,2) DEFAULT 0, reorder_qty DECIMAL(10,2) DEFAULT 0, unit_cost DECIMAL(15,2) DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(tenant_id, product_id, warehouse_id) ); CREATE TABLE IF NOT EXISTS boc_inventory_movements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, product_id UUID REFERENCES boc_products(id), warehouse_id UUID REFERENCES boc_warehouses(id), type TEXT NOT NULL, -- in, out, adjustment, transfer quantity DECIMAL(10,2) NOT NULL, reference_type TEXT, -- order, po, adjustment reference_id TEXT, notes TEXT, created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- RECURRING: Subscriptions & Recurring Invoices -- ============================================ CREATE TABLE IF NOT EXISTS boc_subscription_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, product_id UUID REFERENCES boc_products(id), interval TEXT NOT NULL DEFAULT 'monthly', -- weekly, monthly, quarterly, yearly interval_count INTEGER DEFAULT 1, price DECIMAL(15,2) NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', trial_days INTEGER DEFAULT 0, setup_fee DECIMAL(15,2) DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, customer_id UUID REFERENCES boc_customers(id) ON DELETE CASCADE, plan_id UUID REFERENCES boc_subscription_plans(id), status TEXT NOT NULL DEFAULT 'active', -- active, paused, cancelled, expired start_date DATE NOT NULL, end_date DATE, trial_end DATE, current_period_start DATE, current_period_end DATE, price DECIMAL(15,2) NOT NULL, currency TEXT NOT NULL DEFAULT 'USD', metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_recurring_invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, customer_id UUID REFERENCES boc_customers(id), subscription_id UUID REFERENCES boc_subscriptions(id), plan_id UUID REFERENCES boc_subscription_plans(id), invoice_number TEXT, amount DECIMAL(15,2) NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'USD', status TEXT NOT NULL DEFAULT 'pending', -- pending, generated, sent, paid, failed scheduled_date DATE NOT NULL, generated_at TIMESTAMPTZ, sent_at TIMESTAMPTZ, error TEXT, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- EXPENSES: Receipts & OCR -- ============================================ CREATE TABLE IF NOT EXISTS boc_receipts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, employee_id UUID REFERENCES boc_employees(id), expense_id UUID REFERENCES boc_expenses(id), image_url TEXT NOT NULL, ocr_text TEXT, ocr_data JSONB DEFAULT '{}', -- extracted: amount, date, vendor, category ocr_confidence DECIMAL(5,2) DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', -- pending, processed, failed processed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- PAYROLL: Basic structure -- ============================================ CREATE TABLE IF NOT EXISTS boc_payroll_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, period_start DATE NOT NULL, period_end DATE NOT NULL, pay_date DATE NOT NULL, status TEXT NOT NULL DEFAULT 'draft', -- draft, processing, approved, paid total_gross DECIMAL(15,2) DEFAULT 0, total_tax DECIMAL(15,2) DEFAULT 0, total_net DECIMAL(15,2) DEFAULT 0, total_employer_tax DECIMAL(15,2) DEFAULT 0, currency TEXT DEFAULT 'USD', metadata JSONB DEFAULT '{}', created_by UUID REFERENCES boc_users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_payroll_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, payroll_run_id UUID REFERENCES boc_payroll_runs(id) ON DELETE CASCADE, employee_id UUID REFERENCES boc_employees(id), gross_salary DECIMAL(15,2) NOT NULL DEFAULT 0, tax_deduction DECIMAL(15,2) DEFAULT 0, social_fees DECIMAL(15,2) DEFAULT 0, pension DECIMAL(15,2) DEFAULT 0, other_deductions DECIMAL(15,2) DEFAULT 0, net_salary DECIMAL(15,2) DEFAULT 0, hours_worked DECIMAL(5,2) DEFAULT 0, vacation_days_used DECIMAL(4,1) DEFAULT 0, sick_days DECIMAL(4,1) DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- BANK: Accounts & Transactions -- ============================================ CREATE TABLE IF NOT EXISTS boc_bank_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, bank_name TEXT NOT NULL, account_number TEXT NOT NULL, iban TEXT, bic TEXT, currency TEXT DEFAULT 'USD', balance DECIMAL(15,2) DEFAULT 0, is_default BOOLEAN DEFAULT FALSE, status TEXT NOT NULL DEFAULT 'active', last_sync TIMESTAMPTZ, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_bank_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, account_id UUID REFERENCES boc_bank_accounts(id) ON DELETE CASCADE, transaction_date DATE NOT NULL, amount DECIMAL(15,2) NOT NULL, currency TEXT DEFAULT 'USD', description TEXT, counterparty TEXT, reference TEXT, external_id TEXT, status TEXT NOT NULL DEFAULT 'unmatched', -- unmatched, matched, reconciled matched_to_type TEXT, -- invoice, expense, payroll matched_to_id UUID, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- PROJECTS -- ============================================ CREATE TABLE IF NOT EXISTS boc_projects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, customer_id UUID REFERENCES boc_customers(id), status TEXT NOT NULL DEFAULT 'active', -- active, completed, on_hold, cancelled budget DECIMAL(15,2) DEFAULT 0, spent DECIMAL(15,2) DEFAULT 0, currency TEXT DEFAULT 'USD', start_date DATE, end_date DATE, manager_id UUID REFERENCES boc_employees(id), metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_project_times ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, project_id UUID REFERENCES boc_projects(id) ON DELETE CASCADE, employee_id UUID REFERENCES boc_employees(id), date DATE NOT NULL, hours DECIMAL(4,2) NOT NULL DEFAULT 0, description TEXT, billable BOOLEAN DEFAULT TRUE, hourly_rate DECIMAL(15,2) DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS boc_project_expenses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES boc_tenants(id) ON DELETE CASCADE, project_id UUID REFERENCES boc_projects(id) ON DELETE CASCADE, expense_id UUID REFERENCES boc_expenses(id), amount DECIMAL(15,2) NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexes CREATE INDEX IF NOT EXISTS idx_quotes_tenant ON boc_quotes(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_quotes_customer ON boc_quotes(customer_id); CREATE INDEX IF NOT EXISTS idx_orders_tenant ON boc_orders(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_orders_customer ON boc_orders(customer_id); CREATE INDEX IF NOT EXISTS idx_suppliers_tenant ON boc_suppliers(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_po_tenant ON boc_purchase_orders(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_supplier_invoices_tenant ON boc_supplier_invoices(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_inventory_product ON boc_inventory(product_id, warehouse_id); CREATE INDEX IF NOT EXISTS idx_inventory_movements ON boc_inventory_movements(product_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_subscriptions_customer ON boc_subscriptions(customer_id, status); CREATE INDEX IF NOT EXISTS idx_recurring_invoices ON boc_recurring_invoices(scheduled_date, status); CREATE INDEX IF NOT EXISTS idx_receipts_status ON boc_receipts(status); CREATE INDEX IF NOT EXISTS idx_payroll_runs ON boc_payroll_runs(period_start, status); CREATE INDEX IF NOT EXISTS idx_bank_transactions ON boc_bank_transactions(account_id, transaction_date DESC); CREATE INDEX IF NOT EXISTS idx_projects_tenant ON boc_projects(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_project_times ON boc_project_times(project_id, date); -- Triggers for updated_at DO $$ BEGIN CREATE TRIGGER update_boc_quotes_updated_at BEFORE UPDATE ON boc_quotes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_orders_updated_at BEFORE UPDATE ON boc_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_suppliers_updated_at BEFORE UPDATE ON boc_suppliers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_purchase_orders_updated_at BEFORE UPDATE ON boc_purchase_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_supplier_invoices_updated_at BEFORE UPDATE ON boc_supplier_invoices FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_inventory_updated_at BEFORE UPDATE ON boc_inventory FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_subscription_plans_updated_at BEFORE UPDATE ON boc_subscription_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_subscriptions_updated_at BEFORE UPDATE ON boc_subscriptions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_payroll_runs_updated_at BEFORE UPDATE ON boc_payroll_runs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_bank_accounts_updated_at BEFORE UPDATE ON boc_bank_accounts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_boc_projects_updated_at BEFORE UPDATE ON boc_projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); EXCEPTION WHEN duplicate_object THEN -- Triggers already exist, ignore END; $$;