bmc_hub/migrations/1002_asset_first_subscription_billing.sql

117 lines
6.6 KiB
MySQL
Raw Normal View History

-- 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.';