Files
myeasycms-v2/apps/web/supabase/migrations/20260416000014_notification_rules_and_jobs.sql
T. Zehetbauer 5c5aaabae5
Some checks failed
Workflow / ʦ TypeScript (pull_request) Failing after 5m57s
Workflow / ⚫️ Test (pull_request) Has been skipped
refactor: remove obsolete member management API module
2026-04-03 14:08:31 +02:00

210 lines
7.4 KiB
PL/PgSQL

-- =====================================================
-- 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();