124 lines
6.2 KiB
MySQL
124 lines
6.2 KiB
MySQL
|
|
-- 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);
|