Files
GL/infrastructure/postgres/migrations/000006_gl_authority.up.sql
T
nfel 88e227f80c
CI/CD Stage / stage (push) Failing after 1m12s
feat(gl): add standalone ledger authority mode
2026-10-06 22:40:07 +03:30

96 lines
4.7 KiB
PL/PgSQL

CREATE TABLE ledger_operating_state (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
mode text NOT NULL CHECK (mode IN ('NORMAL','GL_ONLY')),
revision bigint NOT NULL CHECK (revision >= 0),
actor_id text NOT NULL, reason text NOT NULL,
changed_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
INSERT INTO ledger_operating_state (mode,revision,actor_id,reason) VALUES ('NORMAL',0,'system','initial mode');
CREATE TABLE ledger_mode_changes (
revision bigint PRIMARY KEY,
previous_mode text NOT NULL CHECK (previous_mode IN ('NORMAL','GL_ONLY')),
mode text NOT NULL CHECK (mode IN ('NORMAL','GL_ONLY')),
actor_id text NOT NULL CHECK (btrim(actor_id) <> ''),
reason text NOT NULL CHECK (btrim(reason) <> ''),
reconciliation_confirmed boolean NOT NULL, changed_at timestamptz NOT NULL
);
CREATE TRIGGER mode_changes_reject_mutation BEFORE UPDATE OR DELETE ON ledger_mode_changes
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
CREATE TRIGGER operating_state_reject_delete BEFORE DELETE ON ledger_operating_state
FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();
-- A shared lock is held through each posting; toggles drain writers.
CREATE FUNCTION guard_journal_authority() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE current_mode text;
BEGIN
SELECT mode INTO STRICT current_mode FROM ledger_operating_state WHERE singleton FOR SHARE;
IF NEW.metadata->>'gl_execution'='GL_ONLY' AND current_mode<>'GL_ONLY'
AND NOT EXISTS (SELECT 1 FROM journals WHERE idempotency_key=NEW.idempotency_key) THEN
RAISE EXCEPTION 'GL_ONLY mode is required' USING ERRCODE='PGL01';
END IF;
IF current_mode='GL_ONLY' AND COALESCE(NEW.metadata->>'gl_execution','')<>'GL_ONLY'
AND NOT EXISTS (SELECT 1 FROM journals WHERE idempotency_key=NEW.idempotency_key) THEN
RAISE EXCEPTION 'new GL_ONLY effects must use internal execution' USING ERRCODE='PGL01';
END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER journals_guard_authority BEFORE INSERT ON journals
FOR EACH ROW EXECUTE FUNCTION guard_journal_authority();
-- Both credits and debits lock accounts, including normal recorders.
CREATE OR REPLACE FUNCTION guard_entry_insert() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF (SELECT sealed_at IS NOT NULL FROM journals WHERE id=NEW.journal_id) THEN
RAISE EXCEPTION 'cannot append to a sealed journal' USING ERRCODE='55000';
END IF;
PERFORM id FROM ledger_accounts WHERE id=NEW.account_id
AND class IN ('USER_AVAILABLE','USER_FROZEN') FOR UPDATE;
RETURN NEW;
END; $$;
CREATE OR REPLACE FUNCTION guard_journal_seal() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE current_mode text;
BEGIN
IF OLD.sealed_at IS NOT NULL OR NEW.sealed_at IS NULL
OR (to_jsonb(NEW)-'sealed_at') IS DISTINCT FROM (to_jsonb(OLD)-'sealed_at') THEN
RAISE EXCEPTION 'journals are immutable except for initial sealing' USING ERRCODE='55000';
END IF;
IF (SELECT count(*) FROM journal_entries WHERE journal_id=NEW.id)<2 THEN
RAISE EXCEPTION 'journal requires at least two entries' USING ERRCODE='23514';
END IF;
IF EXISTS (SELECT asset_id FROM journal_entries WHERE journal_id=NEW.id GROUP BY asset_id HAVING sum(amount)<>0) THEN
RAISE EXCEPTION 'journal is not balanced per asset' USING ERRCODE='23514';
END IF;
SELECT mode INTO STRICT current_mode FROM ledger_operating_state WHERE singleton;
IF current_mode='GL_ONLY' THEN
IF NEW.blockchain_network<>'' OR NEW.blockchain_transaction_hash<>'' OR NEW.blockchain_ledger_sequence<>''
OR EXISTS (SELECT 1 FROM journal_entries e JOIN ledger_accounts a ON a.id=e.account_id
WHERE e.journal_id=NEW.id AND a.class='EXTERNAL_BLOCKCHAIN') THEN
RAISE EXCEPTION 'external blockchain movements are unavailable in GL_ONLY' USING ERRCODE='PGL01';
END IF;
IF EXISTS (
SELECT a.id FROM ledger_accounts a JOIN journal_entries e ON e.account_id=a.id JOIN journals j ON j.id=e.journal_id
WHERE a.class IN ('USER_AVAILABLE','USER_FROZEN')
AND a.id IN (SELECT account_id FROM journal_entries WHERE journal_id=NEW.id)
AND (j.sealed_at IS NOT NULL OR j.id=NEW.id)
GROUP BY a.id HAVING sum(e.amount)<0
) THEN
RAISE EXCEPTION 'insufficient ledger funds' USING ERRCODE='PGL02';
END IF;
END IF;
RETURN NEW;
END; $$;
CREATE FUNCTION guard_settlement_authority() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE current_mode text;
BEGIN
SELECT mode INTO STRICT current_mode FROM ledger_operating_state WHERE singleton FOR SHARE;
IF current_mode='GL_ONLY'
AND NOT EXISTS (SELECT 1 FROM kuknos_settlements WHERE idempotency_key=NEW.idempotency_key) THEN
RAISE EXCEPTION 'blockchain settlement is disabled in GL_ONLY' USING ERRCODE='PGL01';
END IF;
RETURN NEW;
END; $$;
CREATE TRIGGER settlements_guard_authority BEFORE INSERT ON kuknos_settlements
FOR EACH ROW EXECUTE FUNCTION guard_settlement_authority();