-- Migration 1002: Asset-first subscriptions, billing controls, and price change tracking -- Adds fields and tables needed for asset-linked subscriptions and flexible billing. ALTER TABLE sag_subscriptions ADD COLUMN IF NOT EXISTS billing_direction VARCHAR(20) NOT NULL DEFAULT 'forward' CHECK (billing_direction IN ('forward', 'backward')), ADD COLUMN IF NOT EXISTS advance_months INTEGER NOT NULL DEFAULT 1 CHECK (advance_months >= 1 AND advance_months <= 24), ADD COLUMN IF NOT EXISTS first_full_period_start DATE, ADD COLUMN IF NOT EXISTS binding_months INTEGER NOT NULL DEFAULT 0 CHECK (binding_months >= 0), ADD COLUMN IF NOT EXISTS binding_start_date DATE, ADD COLUMN IF NOT EXISTS binding_end_date DATE, ADD COLUMN IF NOT EXISTS binding_group_key VARCHAR(80), ADD COLUMN IF NOT EXISTS billing_blocked BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS billing_block_reason TEXT, ADD COLUMN IF NOT EXISTS invoice_merge_key VARCHAR(120), ADD COLUMN IF NOT EXISTS price_change_case_id INTEGER REFERENCES sag_sager(id), ADD COLUMN IF NOT EXISTS renewal_case_id INTEGER REFERENCES sag_sager(id); CREATE INDEX IF NOT EXISTS idx_sag_subscriptions_billing_direction ON sag_subscriptions(billing_direction); CREATE INDEX IF NOT EXISTS idx_sag_subscriptions_billing_blocked ON sag_subscriptions(billing_blocked) WHERE billing_blocked = true; CREATE INDEX IF NOT EXISTS idx_sag_subscriptions_binding_end_date ON sag_subscriptions(binding_end_date) WHERE binding_end_date IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_sag_subscriptions_invoice_merge_key ON sag_subscriptions(invoice_merge_key); ALTER TABLE sag_subscription_items ADD COLUMN IF NOT EXISTS asset_id INTEGER REFERENCES hardware_assets(id) ON DELETE RESTRICT, ADD COLUMN IF NOT EXISTS period_from DATE, ADD COLUMN IF NOT EXISTS period_to DATE, ADD COLUMN IF NOT EXISTS requires_serial_number BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS serial_number VARCHAR(100), ADD COLUMN IF NOT EXISTS billing_blocked BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS billing_block_reason TEXT; CREATE INDEX IF NOT EXISTS idx_sag_subscription_items_asset_id ON sag_subscription_items(asset_id); CREATE INDEX IF NOT EXISTS idx_sag_subscription_items_blocked ON sag_subscription_items(billing_blocked) WHERE billing_blocked = true; CREATE TABLE IF NOT EXISTS subscription_asset_bindings ( id SERIAL PRIMARY KEY, subscription_id INTEGER NOT NULL REFERENCES sag_subscriptions(id) ON DELETE CASCADE, asset_id INTEGER NOT NULL REFERENCES hardware_assets(id) ON DELETE RESTRICT, shared_binding_key VARCHAR(80), binding_months INTEGER NOT NULL DEFAULT 0 CHECK (binding_months >= 0), start_date DATE NOT NULL, end_date DATE, notice_period_days INTEGER NOT NULL DEFAULT 30 CHECK (notice_period_days >= 0), status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'ended', 'cancelled')), sag_id INTEGER REFERENCES sag_sager(id), created_by_user_id INTEGER REFERENCES users(user_id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, UNIQUE (subscription_id, asset_id, start_date) ); CREATE INDEX IF NOT EXISTS idx_subscription_asset_bindings_subscription ON subscription_asset_bindings(subscription_id); CREATE INDEX IF NOT EXISTS idx_subscription_asset_bindings_asset ON subscription_asset_bindings(asset_id); CREATE INDEX IF NOT EXISTS idx_subscription_asset_bindings_end_date ON subscription_asset_bindings(end_date); CREATE TRIGGER trigger_subscription_asset_bindings_updated_at BEFORE UPDATE ON subscription_asset_bindings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TABLE IF NOT EXISTS subscription_price_changes ( id SERIAL PRIMARY KEY, subscription_id INTEGER NOT NULL REFERENCES sag_subscriptions(id) ON DELETE CASCADE, subscription_item_id INTEGER REFERENCES sag_subscription_items(id) ON DELETE SET NULL, sag_id INTEGER NOT NULL REFERENCES sag_sager(id) ON DELETE RESTRICT, change_scope VARCHAR(20) NOT NULL DEFAULT 'subscription' CHECK (change_scope IN ('subscription', 'item')), old_unit_price DECIMAL(10,2), new_unit_price DECIMAL(10,2) NOT NULL CHECK (new_unit_price >= 0), effective_date DATE NOT NULL, approval_status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (approval_status IN ('pending', 'approved', 'rejected', 'applied')), reason TEXT, approved_by_user_id INTEGER REFERENCES users(user_id), approved_at TIMESTAMP, created_by_user_id INTEGER REFERENCES users(user_id), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_subscription_price_changes_subscription ON subscription_price_changes(subscription_id); CREATE INDEX IF NOT EXISTS idx_subscription_price_changes_effective_date ON subscription_price_changes(effective_date); CREATE INDEX IF NOT EXISTS idx_subscription_price_changes_status ON subscription_price_changes(approval_status); CREATE TRIGGER trigger_subscription_price_changes_updated_at BEFORE UPDATE ON subscription_price_changes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ALTER TABLE ordre_drafts ADD COLUMN IF NOT EXISTS coverage_start DATE, ADD COLUMN IF NOT EXISTS coverage_end DATE, ADD COLUMN IF NOT EXISTS billing_direction VARCHAR(20) CHECK (billing_direction IN ('forward', 'backward')), ADD COLUMN IF NOT EXISTS source_subscription_ids INTEGER[] NOT NULL DEFAULT '{}', ADD COLUMN IF NOT EXISTS invoice_aggregate_key VARCHAR(120), ADD COLUMN IF NOT EXISTS sync_status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (sync_status IN ('pending', 'exported', 'failed', 'posted', 'paid')), ADD COLUMN IF NOT EXISTS economic_order_number VARCHAR(80), ADD COLUMN IF NOT EXISTS economic_invoice_number VARCHAR(80), ADD COLUMN IF NOT EXISTS last_sync_at TIMESTAMP; CREATE INDEX IF NOT EXISTS idx_ordre_drafts_sync_status ON ordre_drafts(sync_status); CREATE INDEX IF NOT EXISTS idx_ordre_drafts_invoice_aggregate_key ON ordre_drafts(invoice_aggregate_key); ALTER TABLE products ADD COLUMN IF NOT EXISTS serial_number_required BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS asset_required BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS rental_asset_enabled BOOLEAN NOT NULL DEFAULT false; COMMENT ON COLUMN products.serial_number_required IS 'If true, subscription line billing requires serial number data.'; COMMENT ON COLUMN products.asset_required IS 'If true, subscription line billing requires linked hardware asset.'; COMMENT ON COLUMN products.rental_asset_enabled IS 'If true, product is eligible for asset-first rental subscription flows.';