CREATE TABLE IF NOT EXISTS ledger_schema_migrations ( version bigint PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT clock_timestamp() ); CREATE TABLE ledger_accounts ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, class text NOT NULL CHECK (class IN ( 'USER_AVAILABLE', 'USER_FROZEN', 'EXTERNAL_BLOCKCHAIN', 'TREASURY', 'MARKET_CLEARING', 'IPG_CLEARING', 'COMMISSION_REVENUE' )), owner_type text NOT NULL DEFAULT '', owner_id text NOT NULL DEFAULT '', asset_id bigint NOT NULL CHECK (asset_id > 0), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), UNIQUE (class, owner_type, owner_id, asset_id), UNIQUE (id, asset_id), CHECK ( class NOT IN ('USER_AVAILABLE', 'USER_FROZEN') OR (owner_type <> '' AND owner_id <> '') ) ); CREATE TABLE journals ( id uuid PRIMARY KEY, source_service text NOT NULL CHECK (source_service <> ''), idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''), source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''), tracking_code text NOT NULL DEFAULT '', effect_kind text NOT NULL CHECK (effect_kind <> ''), event_version integer NOT NULL CHECK (event_version > 0), reversal_of_journal_id uuid REFERENCES journals (id), occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(), sealed_at timestamptz, correlation_id text NOT NULL DEFAULT '', actor_id text NOT NULL DEFAULT '', blockchain_network text NOT NULL DEFAULT '', blockchain_transaction_hash text NOT NULL DEFAULT '', blockchain_ledger_sequence text NOT NULL DEFAULT '', metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'), payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'), CHECK (reversal_of_journal_id IS NULL OR reversal_of_journal_id <> id) ); CREATE INDEX journals_source_transaction_idx ON journals (source_service, source_transaction_id); CREATE INDEX journals_recorded_at_idx ON journals (recorded_at, id); CREATE UNIQUE INDEX journals_one_reversal_idx ON journals (reversal_of_journal_id) WHERE reversal_of_journal_id IS NOT NULL; CREATE TABLE journal_entries ( journal_id uuid NOT NULL REFERENCES journals (id), line_number integer NOT NULL CHECK (line_number > 0), account_id bigint NOT NULL, asset_id bigint NOT NULL CHECK (asset_id > 0), amount numeric(38, 18) NOT NULL CHECK (amount <> 0), description text NOT NULL DEFAULT '', PRIMARY KEY (journal_id, line_number), FOREIGN KEY (account_id, asset_id) REFERENCES ledger_accounts (id, asset_id) ); CREATE INDEX journal_entries_account_idx ON journal_entries (account_id, journal_id, line_number); CREATE TABLE transaction_events ( id uuid PRIMARY KEY, source_service text NOT NULL CHECK (source_service <> ''), idempotency_key text NOT NULL UNIQUE CHECK (idempotency_key <> ''), source_transaction_id text NOT NULL CHECK (source_transaction_id <> ''), tracking_code text NOT NULL DEFAULT '', event_version integer NOT NULL CHECK (event_version > 0), state text NOT NULL CHECK (state IN ( 'CREATED', 'PENDING_TRANSACTION', 'PENDING_ADMIN', 'SUCCESSFUL', 'FAILED', 'SUSPENDED' )), error_code text NOT NULL DEFAULT '', error_message text NOT NULL DEFAULT '', occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL DEFAULT clock_timestamp(), correlation_id text NOT NULL DEFAULT '', actor_id text NOT NULL DEFAULT '', blockchain_network text NOT NULL DEFAULT '', blockchain_transaction_hash text NOT NULL DEFAULT '', blockchain_ledger_sequence text NOT NULL DEFAULT '', metadata jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(metadata) = 'object'), payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$') ); CREATE INDEX transaction_events_source_idx ON transaction_events (source_service, source_transaction_id, event_version); CREATE FUNCTION reject_ledger_mutation() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION '% is append-only', TG_TABLE_NAME USING ERRCODE = '55000'; END; $$; CREATE FUNCTION guard_journal_seal() RETURNS trigger LANGUAGE plpgsql AS $$ 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; RETURN NEW; END; $$; CREATE 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; RETURN NEW; END; $$; CREATE TRIGGER journals_guard_update BEFORE UPDATE ON journals FOR EACH ROW EXECUTE FUNCTION guard_journal_seal(); CREATE TRIGGER journals_reject_delete BEFORE DELETE ON journals FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation(); CREATE TRIGGER entries_guard_insert BEFORE INSERT ON journal_entries FOR EACH ROW EXECUTE FUNCTION guard_entry_insert(); CREATE TRIGGER entries_reject_update_or_delete BEFORE UPDATE OR DELETE ON journal_entries FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation(); CREATE TRIGGER accounts_reject_update_or_delete BEFORE UPDATE OR DELETE ON ledger_accounts FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation(); CREATE TRIGGER transaction_events_reject_update_or_delete BEFORE UPDATE OR DELETE ON transaction_events FOR EACH ROW EXECUTE FUNCTION reject_ledger_mutation();