210 lines
7.4 KiB
PL/PgSQL
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();
|