-- 003_outbox_state_machine.sql -- Enforce state machine constraints and valid transitions -- Prevents invalid status transitions (e.g., FINALIZED -> QUEUED) ALTER TABLE dual_ledger_outbox ADD CONSTRAINT dual_ledger_outbox_status_check CHECK (status IN ('QUEUED','SENT','ACKED','FINALIZED','FAILED')); CREATE OR REPLACE FUNCTION enforce_outbox_status_transition() RETURNS trigger AS $$ DECLARE allowed boolean := false; BEGIN -- No-op if status unchanged IF OLD.status = NEW.status THEN RETURN NEW; END IF; -- Allowed transitions: -- QUEUED -> SENT | FAILED -- SENT -> ACKED | FAILED -- ACKED -> FINALIZED | FAILED -- FAILED -> QUEUED (retry) | FAILED (no change) IF OLD.status = 'QUEUED' AND NEW.status IN ('SENT','FAILED') THEN allowed := true; ELSIF OLD.status = 'SENT' AND NEW.status IN ('ACKED','FAILED') THEN allowed := true; ELSIF OLD.status = 'ACKED' AND NEW.status IN ('FINALIZED','FAILED') THEN allowed := true; ELSIF OLD.status = 'FAILED' AND NEW.status IN ('QUEUED','FAILED') THEN allowed := true; END IF; IF NOT allowed THEN RAISE EXCEPTION 'Invalid outbox transition: % -> %', OLD.status, NEW.status; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS dual_ledger_outbox_status_transition ON dual_ledger_outbox; CREATE TRIGGER dual_ledger_outbox_status_transition BEFORE UPDATE ON dual_ledger_outbox FOR EACH ROW EXECUTE FUNCTION enforce_outbox_status_transition();