-- Subscription agreement overview, approval workflow and invoice idempotency. ALTER TABLE sag_subscriptions ADD COLUMN IF NOT EXISTS billing_schedule_type VARCHAR(30) NOT NULL DEFAULT 'fixed_day', ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1; ALTER TABLE sag_subscriptions DROP CONSTRAINT IF EXISTS sag_subscriptions_status_check; ALTER TABLE sag_subscriptions ADD CONSTRAINT sag_subscriptions_status_check CHECK (status IN ('draft','scheduled','active','paused','terminating','cancelled','expired','blocked')) NOT VALID; ALTER TABLE sag_subscriptions VALIDATE CONSTRAINT sag_subscriptions_status_check; ALTER TABLE sag_subscriptions DROP CONSTRAINT IF EXISTS sag_subscriptions_billing_schedule_type_check; ALTER TABLE sag_subscriptions ADD CONSTRAINT sag_subscriptions_billing_schedule_type_check CHECK (billing_schedule_type IN ('fixed_day','first_business_day','last_business_day','interval_anchor')); CREATE TABLE IF NOT EXISTS subscription_change_requests ( id BIGSERIAL PRIMARY KEY, main_sag_id INTEGER NOT NULL REFERENCES sag_sager(id) ON DELETE CASCADE, change_sag_id INTEGER NOT NULL REFERENCES sag_sager(id) ON DELETE RESTRICT, status VARCHAR(30) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','pending','approved_scheduled','applying','partially_applied','applied','rejected','failed','cancellation_pending','cancelled')), reason TEXT, effective_date DATE NOT NULL DEFAULT CURRENT_DATE, created_by_user_id INTEGER NOT NULL REFERENCES users(user_id), submitted_at TIMESTAMP, approved_by_user_id INTEGER REFERENCES users(user_id), approved_at TIMESTAMP, rejected_by_user_id INTEGER REFERENCES users(user_id), rejected_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uq_subscription_change_request_open_per_sag ON subscription_change_requests(main_sag_id) WHERE status IN ('draft','pending','approved_scheduled','applying','partially_applied','failed','cancellation_pending'); CREATE UNIQUE INDEX IF NOT EXISTS uq_subscription_change_request_case ON subscription_change_requests(change_sag_id); CREATE TABLE IF NOT EXISTS subscription_change_request_items ( id BIGSERIAL PRIMARY KEY, change_request_id BIGINT NOT NULL REFERENCES subscription_change_requests(id) ON DELETE CASCADE, subscription_id INTEGER NOT NULL REFERENCES sag_subscriptions(id) ON DELETE RESTRICT, base_version INTEGER NOT NULL, before_snapshot JSONB NOT NULL, proposed_snapshot JSONB NOT NULL, apply_status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (apply_status IN ('pending','applied','failed','abandoned')), error_message TEXT, applied_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(change_request_id, subscription_id) ); CREATE INDEX IF NOT EXISTS idx_subscription_change_items_subscription ON subscription_change_request_items(subscription_id); CREATE TABLE IF NOT EXISTS subscription_change_cancellations ( id BIGSERIAL PRIMARY KEY, change_request_id BIGINT NOT NULL REFERENCES subscription_change_requests(id) ON DELETE CASCADE, reason TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')), requested_by_user_id INTEGER NOT NULL REFERENCES users(user_id), decided_by_user_id INTEGER REFERENCES users(user_id), decided_at TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uq_subscription_change_cancellation_pending ON subscription_change_cancellations(change_request_id) WHERE status = 'pending'; CREATE TABLE IF NOT EXISTS subscription_events ( id BIGSERIAL PRIMARY KEY, main_sag_id INTEGER NOT NULL REFERENCES sag_sager(id) ON DELETE CASCADE, subscription_id INTEGER REFERENCES sag_subscriptions(id) ON DELETE SET NULL, change_request_id BIGINT REFERENCES subscription_change_requests(id) ON DELETE SET NULL, event_type VARCHAR(50) NOT NULL, actor_user_id INTEGER REFERENCES users(user_id), details JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_subscription_events_sag_created ON subscription_events(main_sag_id, created_at DESC); CREATE TABLE IF NOT EXISTS subscription_billing_runs ( id BIGSERIAL PRIMARY KEY, subscription_id INTEGER NOT NULL REFERENCES sag_subscriptions(id) ON DELETE RESTRICT, period_start DATE NOT NULL, ordre_draft_id INTEGER REFERENCES ordre_drafts(id) ON DELETE SET NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(subscription_id, period_start) ); INSERT INTO permissions (code, description, category) VALUES ('subscriptions.view', 'View subscriptions and agreement history', 'subscriptions'), ('subscriptions.change_request', 'Create subscription change requests', 'subscriptions'), ('subscriptions.approve', 'Approve and retry subscription changes', 'subscriptions') ON CONFLICT (code) DO UPDATE SET description = EXCLUDED.description, category = EXCLUDED.category; WITH permission_map(subscription_code, case_code) AS ( VALUES ('subscriptions.view', 'cases.view'), ('subscriptions.change_request', 'cases.edit') ) INSERT INTO group_permissions (group_id, permission_id) SELECT DISTINCT gp.group_id, subscription_permission.id FROM group_permissions gp JOIN permissions case_permission ON case_permission.id = gp.permission_id JOIN permission_map mapping ON mapping.case_code = case_permission.code JOIN permissions subscription_permission ON subscription_permission.code = mapping.subscription_code ON CONFLICT DO NOTHING; -- Existing out-of-range days need an explicit review instead of silent correction. UPDATE sag_subscriptions SET billing_blocked = TRUE, billing_block_reason = CONCAT_WS('; ', NULLIF(billing_block_reason, ''), 'Fakturadag 29-31 kræver manuel gennemgang') WHERE billing_day > 28 AND COALESCE(billing_block_reason, '') NOT LIKE '%Fakturadag 29-31 kræver manuel gennemgang%'; CREATE INDEX IF NOT EXISTS idx_sag_subscriptions_sag_lifecycle ON sag_subscriptions(sag_id, status, start_date, end_date);