-- ============================================================ -- Migration 004: Constraints y triggers de privacidad -- -- 1. Trigger: rechazar respondent_id no nulo en encuestas anónimas -- → Regla no negociable #2 de CLAUDE.md (barrera técnica) -- -- 2. Trigger: inmutabilidad de consent_records -- → ADR-003: sin UPDATE/DELETE, evidencia permanente del consentimiento -- -- 3. Trigger: inmutabilidad de consent_versions -- → Textos de consentimiento históricos son inmutables -- ============================================================ -- ============================================================ -- 1. Trigger: respondent_id prohibido en encuestas anónimas -- ============================================================ CREATE OR REPLACE FUNCTION public.enforce_anonymous_no_respondent_id() RETURNS trigger LANGUAGE plpgsql SET search_path = public AS $$ DECLARE v_is_anonymous boolean; BEGIN -- Leer la propiedad de la encuesta referida SELECT is_anonymous INTO v_is_anonymous FROM public.surveys WHERE id = NEW.survey_id; IF v_is_anonymous AND NEW.respondent_id IS NOT NULL THEN RAISE EXCEPTION 'Privacy violation: respondent_id must be NULL in anonymous surveys (survey_id: %). ' 'See CLAUDE.md rule #2.', NEW.survey_id USING ERRCODE = 'check_violation'; END IF; RETURN NEW; END; $$; CREATE TRIGGER trg_responses_anonymous_no_respondent_id BEFORE INSERT OR UPDATE ON public.responses FOR EACH ROW EXECUTE FUNCTION public.enforce_anonymous_no_respondent_id(); -- ============================================================ -- 2. Trigger: inmutabilidad de consent_records (ADR-003) -- El consentimiento es evidencia, no UI. Nunca se modifica. -- ============================================================ CREATE OR REPLACE FUNCTION public.deny_consent_records_mutation() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'consent_records is immutable: % is not permitted. ' 'Each response consent is permanent evidence (ADR-003).', TG_OP USING ERRCODE = 'insufficient_privilege'; END; $$; CREATE TRIGGER trg_consent_records_immutable BEFORE UPDATE OR DELETE ON public.consent_records FOR EACH ROW EXECUTE FUNCTION public.deny_consent_records_mutation(); -- ============================================================ -- 3. Trigger: inmutabilidad de consent_versions -- Los textos históricos no se alteran. Cambiar el wording del -- consentimiento crea una nueva versión, nunca edita la existente. -- ============================================================ CREATE OR REPLACE FUNCTION public.deny_consent_versions_mutation() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'consent_versions is immutable: % is not permitted. ' 'Create a new version instead of modifying an existing one.', TG_OP USING ERRCODE = 'insufficient_privilege'; END; $$; CREATE TRIGGER trg_consent_versions_immutable BEFORE UPDATE OR DELETE ON public.consent_versions FOR EACH ROW EXECUTE FUNCTION public.deny_consent_versions_mutation();