-- OMS·WMS·ERP Unified Platform Database Schema v1.0 -- PostgreSQL 15+ -- Principles Applied: -- 3: Data Consistency (SSOT) -- 5: Normalization (3NF minimum) -- 6: Denormalization (performance-justified only) -- 14: Traceability (audit_log on all mutations) -- 23: Security (NUMERIC for financial precision) CREATE SCHEMA IF NOT EXISTS quantengine; SET search_path = quantengine, public; -- ===== ENUMS (Type Safety - Principle 19) ===== CREATE TYPE order_status AS ENUM ('DRAFT', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED'); CREATE TYPE order_line_status AS ENUM ('PENDING', 'ALLOCATED', 'SHIPPED', 'CANCELLED'); CREATE TYPE transfer_status AS ENUM ('REQUESTED', 'APPROVED', 'SHIPPED', 'RECEIVED', 'CANCELLED'); CREATE TYPE product_status AS ENUM ('ACTIVE', 'INACTIVE', 'OBSOLETE'); CREATE TYPE supplier_status AS ENUM ('ACTIVE', 'INACTIVE', 'SUSPENDED'); CREATE TYPE customer_status AS ENUM ('ACTIVE', 'INACTIVE', 'SUSPENDED'); CREATE TYPE gl_account_type AS ENUM ('ASSET', 'LIABILITY', 'EQUITY', 'REVENUE', 'EXPENSE'); CREATE TYPE voucher_status AS ENUM ('DRAFT', 'POSTED', 'APPROVED', 'VOIDED'); CREATE TYPE voucher_document_type AS ENUM ('PURCHASE', 'SALES', 'JOURNAL', 'ADJUSTMENT'); CREATE TYPE inventory_status AS ENUM ('ACTIVE', 'INACTIVE', 'DAMAGED'); CREATE TYPE unit_of_measure AS ENUM ('EA', 'KG', 'M', 'L', 'BOX'); CREATE TYPE audit_operation AS ENUM ('CREATE', 'UPDATE', 'DELETE'); CREATE TYPE user_role AS ENUM ('ADMIN', 'MANAGER', 'OPERATOR', 'VIEWER', 'ANALYST'); -- ===== COMMON/MASTER TABLES ===== CREATE TABLE users ( user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, role user_role NOT NULL DEFAULT 'VIEWER', status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', created_by UUID, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_users_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT fk_users_modified_by FOREIGN KEY (modified_by) REFERENCES users(user_id), CONSTRAINT fk_users_deleted_by FOREIGN KEY (deleted_by) REFERENCES users(user_id) ); CREATE INDEX idx_users_email ON users(email); CREATE TABLE warehouses ( warehouse_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), warehouse_code VARCHAR(50) NOT NULL UNIQUE, warehouse_name VARCHAR(255) NOT NULL, location VARCHAR(255), status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_warehouses_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE INDEX idx_warehouses_code ON warehouses(warehouse_code); CREATE TABLE product_categories ( category_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category_name VARCHAR(255) NOT NULL UNIQUE, description TEXT, status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_categories_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); -- ===== OMS: ORDER MANAGEMENT ===== CREATE TABLE customers ( customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_code VARCHAR(50) NOT NULL UNIQUE, customer_name VARCHAR(255) NOT NULL, email VARCHAR(255), phone VARCHAR(20), business_registration VARCHAR(50), status customer_status NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_customers_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE INDEX idx_customers_code ON customers(customer_code); CREATE INDEX idx_customers_email ON customers(email); CREATE TABLE orders ( order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_no VARCHAR(50) NOT NULL UNIQUE, customer_id UUID NOT NULL, order_date DATE NOT NULL DEFAULT CURRENT_DATE, total_amount NUMERIC(19,4) NOT NULL DEFAULT 0.0, status order_status NOT NULL DEFAULT 'DRAFT', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id), CONSTRAINT fk_orders_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT chk_total_amount CHECK (total_amount >= 0) ); CREATE INDEX idx_orders_no ON orders(order_no); CREATE INDEX idx_orders_customer_id ON orders(customer_id); CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_orders_order_date ON orders(order_date); CREATE TABLE order_lines ( line_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL, line_no SMALLINT NOT NULL, product_id UUID NOT NULL, quantity NUMERIC(19,4) NOT NULL, quantity_unit unit_of_measure NOT NULL, unit_price NUMERIC(19,4) NOT NULL, line_total NUMERIC(19,4) NOT NULL, status order_line_status NOT NULL DEFAULT 'PENDING', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_order_lines_order FOREIGN KEY (order_id) REFERENCES orders(order_id), CONSTRAINT fk_order_lines_product FOREIGN KEY (product_id) REFERENCES products(product_id), CONSTRAINT fk_order_lines_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT uk_order_lines_order_lineno UNIQUE (order_id, line_no), CONSTRAINT chk_quantity CHECK (quantity > 0), CONSTRAINT chk_unit_price CHECK (unit_price >= 0), CONSTRAINT chk_line_total CHECK (line_total >= 0) ); CREATE INDEX idx_order_lines_order_id ON order_lines(order_id); CREATE INDEX idx_order_lines_product_id ON order_lines(product_id); -- ===== WMS: WAREHOUSE MANAGEMENT ===== CREATE TABLE inventory ( inventory_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), warehouse_id UUID NOT NULL, product_id UUID NOT NULL, qty_on_hand NUMERIC(19,4) NOT NULL DEFAULT 0.0, qty_reserved NUMERIC(19,4) NOT NULL DEFAULT 0.0, qty_available NUMERIC(19,4) NOT NULL GENERATED ALWAYS AS (qty_on_hand - qty_reserved) STORED, last_adjustment_date TIMESTAMP, status inventory_status NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_inventory_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouses(warehouse_id), CONSTRAINT fk_inventory_product FOREIGN KEY (product_id) REFERENCES products(product_id), CONSTRAINT fk_inventory_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT uk_inventory_warehouse_product UNIQUE (warehouse_id, product_id), CONSTRAINT chk_qty_on_hand CHECK (qty_on_hand >= 0), CONSTRAINT chk_qty_reserved CHECK (qty_reserved >= 0) ); CREATE INDEX idx_inventory_warehouse_id ON inventory(warehouse_id); CREATE INDEX idx_inventory_product_id ON inventory(product_id); CREATE TABLE stock_transfers ( transfer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), transfer_no VARCHAR(50) NOT NULL UNIQUE, from_warehouse_id UUID NOT NULL, to_warehouse_id UUID NOT NULL, product_id UUID NOT NULL, quantity NUMERIC(19,4) NOT NULL, reason TEXT, status transfer_status NOT NULL DEFAULT 'REQUESTED', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_transfers_from_warehouse FOREIGN KEY (from_warehouse_id) REFERENCES warehouses(warehouse_id), CONSTRAINT fk_transfers_to_warehouse FOREIGN KEY (to_warehouse_id) REFERENCES warehouses(warehouse_id), CONSTRAINT fk_transfers_product FOREIGN KEY (product_id) REFERENCES products(product_id), CONSTRAINT fk_transfers_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT chk_quantity CHECK (quantity > 0), CONSTRAINT chk_different_warehouses CHECK (from_warehouse_id != to_warehouse_id) ); CREATE INDEX idx_stock_transfers_no ON stock_transfers(transfer_no); CREATE INDEX idx_stock_transfers_from_warehouse ON stock_transfers(from_warehouse_id); CREATE INDEX idx_stock_transfers_to_warehouse ON stock_transfers(to_warehouse_id); CREATE INDEX idx_stock_transfers_status ON stock_transfers(status); -- ===== ERP: ENTERPRISE RESOURCE PLANNING ===== CREATE TABLE products ( product_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), sku VARCHAR(50) NOT NULL UNIQUE, product_name VARCHAR(255) NOT NULL, category_id UUID, unit_of_measure unit_of_measure NOT NULL DEFAULT 'EA', status product_status NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_products_category FOREIGN KEY (category_id) REFERENCES product_categories(category_id), CONSTRAINT fk_products_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE INDEX idx_products_sku ON products(sku); CREATE INDEX idx_products_status ON products(status); CREATE TABLE suppliers ( supplier_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), supplier_code VARCHAR(50) NOT NULL UNIQUE, supplier_name VARCHAR(255) NOT NULL, email VARCHAR(255), phone VARCHAR(20), business_registration VARCHAR(50), status supplier_status NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_suppliers_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE INDEX idx_suppliers_code ON suppliers(supplier_code); CREATE TABLE gl_accounts ( account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_code VARCHAR(50) NOT NULL UNIQUE, account_name VARCHAR(255) NOT NULL, account_type gl_account_type NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_gl_accounts_created_by FOREIGN KEY (created_by) REFERENCES users(user_id) ); CREATE INDEX idx_gl_accounts_code ON gl_accounts(account_code); CREATE INDEX idx_gl_accounts_type ON gl_accounts(account_type); CREATE TABLE vouchers ( voucher_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), voucher_no VARCHAR(50) NOT NULL UNIQUE, document_date DATE NOT NULL, document_type voucher_document_type NOT NULL, description TEXT, total_debit NUMERIC(19,4) NOT NULL DEFAULT 0.0, total_credit NUMERIC(19,4) NOT NULL DEFAULT 0.0, status voucher_status NOT NULL DEFAULT 'DRAFT', created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, modified_by UUID, modified_at TIMESTAMP, deleted_by UUID, deleted_at TIMESTAMP, CONSTRAINT fk_vouchers_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT chk_totals_balance CHECK (total_debit = total_credit) ); CREATE INDEX idx_vouchers_no ON vouchers(voucher_no); CREATE INDEX idx_vouchers_document_date ON vouchers(document_date); CREATE INDEX idx_vouchers_status ON vouchers(status); CREATE TABLE voucher_lines ( voucher_line_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), voucher_id UUID NOT NULL, line_no SMALLINT NOT NULL, account_id UUID NOT NULL, debit_amount NUMERIC(19,4) NOT NULL DEFAULT 0.0, credit_amount NUMERIC(19,4) NOT NULL DEFAULT 0.0, description TEXT, created_by UUID NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_voucher_lines_voucher FOREIGN KEY (voucher_id) REFERENCES vouchers(voucher_id), CONSTRAINT fk_voucher_lines_account FOREIGN KEY (account_id) REFERENCES gl_accounts(account_id), CONSTRAINT fk_voucher_lines_created_by FOREIGN KEY (created_by) REFERENCES users(user_id), CONSTRAINT uk_voucher_lines_voucher_lineno UNIQUE (voucher_id, line_no), CONSTRAINT chk_debit_amount CHECK (debit_amount >= 0), CONSTRAINT chk_credit_amount CHECK (credit_amount >= 0), CONSTRAINT chk_either_debit_or_credit CHECK ((debit_amount > 0 OR credit_amount > 0) AND NOT (debit_amount > 0 AND credit_amount > 0)) ); CREATE INDEX idx_voucher_lines_voucher_id ON voucher_lines(voucher_id); CREATE INDEX idx_voucher_lines_account_id ON voucher_lines(account_id); -- ===== AUDIT TRAIL (Principle 14: Traceability) ===== CREATE TABLE audit_logs ( audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_type VARCHAR(50) NOT NULL, entity_id UUID NOT NULL, operation audit_operation NOT NULL, old_value JSONB, new_value JSONB, changed_by UUID NOT NULL, changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reason TEXT, CONSTRAINT fk_audit_logs_changed_by FOREIGN KEY (changed_by) REFERENCES users(user_id) ); CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id); CREATE INDEX idx_audit_logs_changed_at ON audit_logs(changed_at); CREATE INDEX idx_audit_logs_operation ON audit_logs(operation); -- ===== AUDIT TRIGGER (Principle 14: Automatic Traceability) ===== CREATE OR REPLACE FUNCTION audit_trigger() RETURNS TRIGGER AS $$ DECLARE v_entity_type VARCHAR; v_operation audit_operation; BEGIN v_entity_type := TG_TABLE_NAME; IF TG_OP = 'INSERT' THEN v_operation := 'CREATE'::audit_operation; INSERT INTO audit_logs (entity_type, entity_id, operation, new_value, changed_by, changed_at) VALUES (v_entity_type, NEW.id, v_operation, to_jsonb(NEW), NEW.created_by, CURRENT_TIMESTAMP); RETURN NEW; ELSIF TG_OP = 'UPDATE' THEN v_operation := 'UPDATE'::audit_operation; INSERT INTO audit_logs (entity_type, entity_id, operation, old_value, new_value, changed_by, changed_at) VALUES (v_entity_type, OLD.id, v_operation, to_jsonb(OLD), to_jsonb(NEW), NEW.modified_by, CURRENT_TIMESTAMP); RETURN NEW; ELSIF TG_OP = 'DELETE' THEN v_operation := 'DELETE'::audit_operation; INSERT INTO audit_logs (entity_type, entity_id, operation, old_value, changed_by, changed_at) VALUES (v_entity_type, OLD.id, v_operation, to_jsonb(OLD), OLD.deleted_by, CURRENT_TIMESTAMP); RETURN OLD; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; -- Note: Trigger creation for individual tables omitted to keep schema focused. -- In production: CREATE TRIGGER for orders, inventory, products, etc. -- ===== VIEWS (Convenience, Principle 3: Data Consistency) ===== CREATE VIEW v_order_summary AS SELECT o.order_id, o.order_no, c.customer_name, o.order_date, COUNT(ol.line_id) as line_count, SUM(ol.line_total) as calculated_total, o.total_amount, o.status, o.created_at FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id LEFT JOIN order_lines ol ON o.order_id = ol.order_id WHERE o.deleted_at IS NULL GROUP BY o.order_id, o.order_no, c.customer_name, o.order_date, o.total_amount, o.status, o.created_at; CREATE VIEW v_inventory_summary AS SELECT i.warehouse_id, w.warehouse_name, i.product_id, p.sku, p.product_name, i.qty_on_hand, i.qty_reserved, i.qty_available, i.status, i.modified_at FROM inventory i LEFT JOIN warehouses w ON i.warehouse_id = w.warehouse_id LEFT JOIN products p ON i.product_id = p.product_id WHERE i.deleted_at IS NULL; CREATE VIEW v_voucher_totals AS SELECT v.voucher_id, v.voucher_no, v.document_type, v.document_date, SUM(COALESCE(vl.debit_amount, 0)) as calculated_debit, SUM(COALESCE(vl.credit_amount, 0)) as calculated_credit, v.total_debit, v.total_credit, v.status, COUNT(vl.voucher_line_id) as line_count FROM vouchers v LEFT JOIN voucher_lines vl ON v.voucher_id = vl.voucher_id WHERE v.deleted_at IS NULL GROUP BY v.voucher_id, v.voucher_no, v.document_type, v.document_date, v.total_debit, v.total_credit, v.status; -- ===== INITIAL DATA (Seed) ===== -- Create initial admin user INSERT INTO users (user_id, email, password_hash, name, role, status, created_by, created_at) VALUES ( '00000000-0000-0000-0000-000000000001'::UUID, 'admin@quantengine.dev', '$2b$12$R9h7cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUe', -- bcrypt: admin123! 'System Administrator', 'ADMIN', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP ) ON CONFLICT DO NOTHING; -- Create initial warehouses INSERT INTO warehouses (warehouse_code, warehouse_name, location, status, created_by, created_at) VALUES ('WH-SEOUL', 'Seoul Main Warehouse', 'Seoul, KR', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP), ('WH-BUSAN', 'Busan Distribution Center', 'Busan, KR', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP), ('WH-INCHEON', 'Incheon Port Warehouse', 'Incheon, KR', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP) ON CONFLICT DO NOTHING; -- Create initial product category INSERT INTO product_categories (category_name, description, status, created_by, created_at) VALUES ('Standard Products', 'Regular inventory items', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP), ('Premium Products', 'High-value items', 'ACTIVE', '00000000-0000-0000-0000-000000000001'::UUID, CURRENT_TIMESTAMP) ON CONFLICT DO NOTHING; -- ===== GRANTS (Security - Principle 23) ===== -- Application role (read-write for normal operations) CREATE ROLE quantengine_app LOGIN PASSWORD 'CHANGE_ME_PROD'; GRANT USAGE ON SCHEMA quantengine TO quantengine_app; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA quantengine TO quantengine_app; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA quantengine TO quantengine_app; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA quantengine TO quantengine_app; -- Read-only role (for analytics/reporting) CREATE ROLE quantengine_readonly LOGIN PASSWORD 'CHANGE_ME_PROD'; GRANT USAGE ON SCHEMA quantengine TO quantengine_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA quantengine TO quantengine_readonly; GRANT SELECT ON ALL VIEWS IN SCHEMA quantengine TO quantengine_readonly; -- ===== COMMENTS (Documentation - Principle 28) ===== COMMENT ON SCHEMA quantengine IS 'OMS·WMS·ERP Unified Platform - Phase 0 Database Schema'; COMMENT ON TABLE orders IS 'Order master records (OMS) - Principle 14: Complete audit trail via audit_logs'; COMMENT ON TABLE inventory IS 'Warehouse inventory positions (WMS) - qty_available computed from on_hand - reserved'; COMMENT ON TABLE audit_logs IS 'Universal change audit trail - every mutation logged for compliance + recovery (Principle 14)'; COMMENT ON TABLE vouchers IS 'Accounting journal entries (ERP) - Principle 23: NUMERIC(19,4) for decimal precision';