-- S5: each cause closes separately. No administrator unfreeze switch. CREATE TABLE loan_freeze_reasons ( id UUID PRIMARY KEY, lender_id UUID NOT NULL REFERENCES loan_lenders(id) ON DELETE RESTRICT, payment_batch_id UUID NOT NULL REFERENCES payment_batches(id) ON DELETE RESTRICT, kind VARCHAR(32) NOT NULL CHECK(kind IN ('late_success','reservation_conflict','receivable')), source_key VARCHAR(160) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'active' CHECK(status IN ('active','closed')), created_by UUID NOT NULL REFERENCES employees(id) ON DELETE RESTRICT, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, closed_by UUID REFERENCES employees(id) ON DELETE RESTRICT, closed_at TIMESTAMP(3), closure_basis JSONB, UNIQUE(kind,source_key), CHECK((status='closed')=(closed_by IS NOT NULL AND closed_at IS NOT NULL AND closure_basis IS NOT NULL)) ); CREATE INDEX loan_freeze_reasons_lender_id_status_idx ON loan_freeze_reasons(lender_id,status); CREATE FUNCTION guard_loan_freeze_reason() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE lender UUID; BEGIN IF TG_OP='DELETE' THEN RAISE EXCEPTION 'freeze history retained' USING ERRCODE='23514'; END IF; SELECT r.lender_id INTO lender FROM payment_batches b JOIN loan_repayments r ON r.id=b.loan_repayment_id WHERE b.id=NEW.payment_batch_id; IF lender IS DISTINCT FROM NEW.lender_id THEN RAISE EXCEPTION 'freeze source mismatch' USING ERRCODE='23514'; END IF; PERFORM id FROM loan_lenders WHERE id=lender FOR UPDATE; IF TG_OP='INSERT' THEN IF NEW.status<>'active' OR NEW.closed_by IS NOT NULL OR NEW.closed_at IS NOT NULL OR NEW.closure_basis IS NOT NULL THEN RAISE EXCEPTION 'new freeze must be active' USING ERRCODE='23514'; END IF; ELSIF OLD.status<>'active' OR NEW.status<>'closed' OR (to_jsonb(NEW)-ARRAY['status','closed_by','closed_at','closure_basis']) IS DISTINCT FROM (to_jsonb(OLD)-ARRAY['status','closed_by','closed_at','closure_basis']) THEN RAISE EXCEPTION 'freeze history immutable' USING ERRCODE='23514'; END IF; RETURN NEW; END $$; CREATE TRIGGER loan_freeze_history BEFORE INSERT OR UPDATE OR DELETE ON loan_freeze_reasons FOR EACH ROW EXECUTE FUNCTION guard_loan_freeze_reason();