bmc_hub/migrations/1019_subscription_change_workflow.sql
2026-08-28 20:49:55 +02:00

124 lines
6.2 KiB
SQL

-- 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);