/** * POS-002 — Add `source`, `terminal_id`, `cash_session_id` to `orders_orders`; * drop NOT NULL on `user_id`; add `COMPLETED` to the state CHECK. * * The `source` column distinguishes ecommerce / pos / admin sales. POS sales * may have `user_id IS NULL` (walk-in customer); ecommerce and admin sales * must have a non-null `user_id` (enforced by the application layer; this * migration only relaxes the column). * * The `terminal_id` and `cash_session_id` FKs use `ON DELETE SET NULL` so * decommissioning a terminal or closing its session does not delete * historical sales. * * The state CHECK is rewritten with the new `COMPLETED` value. POS sales * transition PENDING → AWAITING_PAYMENT → PAID → COMPLETED, skipping * SHIPPED / DELIVERED. * * @param {import('node-pg-migrate').MigrationBuilder} pgm */ export const up = (pgm) => { // ── Add columns ────────────────────────────────────────────────────────── pgm.sql(` ALTER TABLE orders_orders ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'ecommerce' `); pgm.sql(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'orders_orders_source_check' ) THEN ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_source_check CHECK (source IN ('ecommerce','pos','admin')); END IF; END $$ `); pgm.sql(`ALTER TABLE orders_orders ADD COLUMN IF NOT EXISTS terminal_id uuid`); pgm.sql(`ALTER TABLE orders_orders ADD COLUMN IF NOT EXISTS cash_session_id uuid`); pgm.sql(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'orders_orders_terminal_id_fkey' ) THEN ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_terminal_id_fkey FOREIGN KEY (terminal_id) REFERENCES pos_terminals(id) ON DELETE SET NULL; END IF; END $$ `); pgm.sql(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'orders_orders_cash_session_id_fkey' ) THEN ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_cash_session_id_fkey FOREIGN KEY (cash_session_id) REFERENCES pos_cash_sessions(id) ON DELETE SET NULL; END IF; END $$ `); // ── Indexes ────────────────────────────────────────────────────────────── pgm.createIndex('orders_orders', 'source', { name: 'orders_orders_source_idx' }); pgm.createIndex('orders_orders', 'terminal_id', { name: 'orders_orders_terminal_idx', where: 'terminal_id IS NOT NULL', }); pgm.createIndex('orders_orders', 'cash_session_id', { name: 'orders_orders_cash_session_idx', where: 'cash_session_id IS NOT NULL', }); // ── Walk-in customers (POS only) ───────────────────────────────────────── pgm.sql(`ALTER TABLE orders_orders ALTER COLUMN user_id DROP NOT NULL`); // CHECK: walk-in customers (user_id NULL) are allowed only for POS sales. // Ecommerce and admin sales must have a non-null user_id. DB-level invariant // enforced in addition to the application-level check. pgm.sql(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'orders_orders_user_required_when_not_pos' ) THEN ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_user_required_when_not_pos CHECK (source = 'pos' OR user_id IS NOT NULL); END IF; END $$ `); // ── Add COMPLETED to state CHECK ───────────────────────────────────────── pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_state_check`); pgm.sql(` ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_state_check CHECK (state IN ( 'PENDING', 'AWAITING_PAYMENT', 'PAID', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'COMPLETED', 'CANCELLED', 'REFUNDED', 'PARTIALLY_REFUNDED' )) `); }; export const down = (pgm) => { // Re-tighten state CHECK to the pre-COMPLETED set; this will fail if any // COMPLETED rows exist. pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_state_check`); pgm.sql(` ALTER TABLE orders_orders ADD CONSTRAINT orders_orders_state_check CHECK (state IN ( 'PENDING','AWAITING_PAYMENT','PAID','PROCESSING', 'SHIPPED','DELIVERED','CANCELLED','REFUNDED','PARTIALLY_REFUNDED' )) `); // user_id becomes NOT NULL again; fails if any NULL rows exist. pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_user_required_when_not_pos`); pgm.sql(`ALTER TABLE orders_orders ALTER COLUMN user_id SET NOT NULL`); pgm.sql(`DROP INDEX IF EXISTS orders_orders_cash_session_idx`); pgm.sql(`DROP INDEX IF EXISTS orders_orders_terminal_idx`); pgm.sql(`DROP INDEX IF EXISTS orders_orders_source_idx`); pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_cash_session_id_fkey`); pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_terminal_id_fkey`); pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS cash_session_id`); pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS terminal_id`); pgm.sql(`ALTER TABLE orders_orders DROP CONSTRAINT IF EXISTS orders_orders_source_check`); pgm.sql(`ALTER TABLE orders_orders DROP COLUMN IF EXISTS source`); };