-- ===================================================== -- Notification Rules + Scheduled Jobs -- -- Configurable notification triggers per account. -- Scheduled job runner with tracking. -- Pending notifications queue for async dispatch. -- ===================================================== -- 1. Notification rules — configurable triggers per account CREATE TABLE IF NOT EXISTS public.member_notification_rules ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, trigger_event text NOT NULL CHECK (trigger_event IN ( 'application.submitted', 'application.approved', 'application.rejected', 'member.created', 'member.status_changed', 'member.birthday', 'member.anniversary', 'dues.unpaid', 'mandate.revoked' )), channel text NOT NULL DEFAULT 'in_app' CHECK (channel IN ('in_app', 'email', 'both')), recipient_type text NOT NULL CHECK (recipient_type IN ( 'admin', 'member', 'specific_user', 'role_holder' )), recipient_config jsonb NOT NULL DEFAULT '{}', subject_template text, message_template text NOT NULL, is_active boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX ix_notification_rules_account ON public.member_notification_rules(account_id, trigger_event) WHERE is_active = true; ALTER TABLE public.member_notification_rules ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.member_notification_rules FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.member_notification_rules TO authenticated; GRANT ALL ON public.member_notification_rules TO service_role; CREATE POLICY notification_rules_select ON public.member_notification_rules FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY notification_rules_mutate ON public.member_notification_rules FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); -- 2. Scheduled job configuration per account CREATE TABLE IF NOT EXISTS public.scheduled_job_configs ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, job_type text NOT NULL CHECK (job_type IN ( 'birthday_notification', 'anniversary_notification', 'dues_reminder', 'data_quality_check', 'gdpr_retention_check' )), is_enabled boolean NOT NULL DEFAULT true, config jsonb NOT NULL DEFAULT '{}', last_run_at timestamptz, next_run_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), UNIQUE(account_id, job_type) ); ALTER TABLE public.scheduled_job_configs ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.scheduled_job_configs FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE ON public.scheduled_job_configs TO authenticated; GRANT ALL ON public.scheduled_job_configs TO service_role; CREATE POLICY scheduled_jobs_select ON public.scheduled_job_configs FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY scheduled_jobs_mutate ON public.scheduled_job_configs FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'members.write'::public.app_permissions)); -- 3. Job run history CREATE TABLE IF NOT EXISTS public.scheduled_job_runs ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), job_config_id uuid NOT NULL REFERENCES public.scheduled_job_configs(id) ON DELETE CASCADE, status text NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')), result jsonb, started_at timestamptz NOT NULL DEFAULT now(), completed_at timestamptz ); CREATE INDEX ix_job_runs_config ON public.scheduled_job_runs(job_config_id, started_at DESC); ALTER TABLE public.scheduled_job_runs ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.scheduled_job_runs FROM authenticated, service_role; GRANT SELECT ON public.scheduled_job_runs TO authenticated; GRANT ALL ON public.scheduled_job_runs TO service_role; CREATE POLICY job_runs_select ON public.scheduled_job_runs FOR SELECT TO authenticated USING (EXISTS ( SELECT 1 FROM public.scheduled_job_configs jc WHERE jc.id = scheduled_job_runs.job_config_id AND public.has_role_on_account(jc.account_id) )); -- 4. Pending notifications queue (lightweight, processed by cron) CREATE TABLE IF NOT EXISTS public.pending_member_notifications ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, account_id uuid NOT NULL, trigger_event text NOT NULL, member_id uuid, context jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz ); CREATE INDEX ix_pending_notifications_unprocessed ON public.pending_member_notifications(created_at) WHERE processed_at IS NULL; -- No RLS — only service_role accesses this table REVOKE ALL ON public.pending_member_notifications FROM authenticated; GRANT ALL ON public.pending_member_notifications TO service_role; -- 5. Trigger: queue notifications when audit events fire CREATE OR REPLACE FUNCTION public.queue_notification_on_audit() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_event text; BEGIN -- Map audit action to notification trigger event v_event := CASE NEW.action WHEN 'created' THEN 'member.created' WHEN 'status_changed' THEN 'member.status_changed' WHEN 'application_approved' THEN 'application.approved' WHEN 'application_rejected' THEN 'application.rejected' ELSE NULL END; IF v_event IS NULL THEN RETURN NEW; END IF; -- Only queue if there are active rules for this event IF EXISTS ( SELECT 1 FROM public.member_notification_rules WHERE account_id = NEW.account_id AND trigger_event = v_event AND is_active = true ) THEN INSERT INTO public.pending_member_notifications (account_id, trigger_event, member_id, context) VALUES ( NEW.account_id, v_event, NEW.member_id, jsonb_build_object( 'audit_action', NEW.action, 'changes', NEW.changes, 'metadata', NEW.metadata, 'user_id', NEW.user_id ) ); END IF; RETURN NEW; END; $$; CREATE TRIGGER trg_audit_queue_notifications AFTER INSERT ON public.member_audit_log FOR EACH ROW EXECUTE FUNCTION public.queue_notification_on_audit(); -- 6. Queue trigger for application submissions (from membership_applications, not audit log) CREATE OR REPLACE FUNCTION public.queue_notification_on_application() RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ BEGIN IF NEW.status = 'submitted' AND (TG_OP = 'INSERT' OR OLD.status IS DISTINCT FROM NEW.status) THEN IF EXISTS ( SELECT 1 FROM public.member_notification_rules WHERE account_id = NEW.account_id AND trigger_event = 'application.submitted' AND is_active = true ) THEN INSERT INTO public.pending_member_notifications (account_id, trigger_event, context) VALUES ( NEW.account_id, 'application.submitted', jsonb_build_object( 'application_id', NEW.id, 'first_name', NEW.first_name, 'last_name', NEW.last_name, 'email', NEW.email ) ); END IF; END IF; RETURN NEW; END; $$; CREATE TRIGGER trg_application_queue_notifications AFTER INSERT OR UPDATE OF status ON public.membership_applications FOR EACH ROW EXECUTE FUNCTION public.queue_notification_on_application();