finalized reminder notification system migrations

This commit is contained in:
2026-03-21 12:58:38 +08:00
parent b2c3202317
commit 7d9096963a
5 changed files with 530 additions and 27 deletions
@@ -73,7 +73,7 @@ BEGIN
FROM public.duty_schedules
WHERE start_time BETWEEN now() + interval '15 minutes' - interval '90 seconds'
AND now() + interval '15 minutes' + interval '90 seconds'
AND status = 'active'
AND status IN ('arrival', 'late')
LOOP
-- Skip if user already checked in for this duty
IF EXISTS (
@@ -106,6 +106,7 @@ BEGIN
FROM public.duty_schedules ds
JOIN public.attendance_logs al ON al.duty_schedule_id = ds.id AND al.user_id = ds.user_id
WHERE ds.end_time < now()
AND ds.end_time >= now() - interval '24 hours'
AND ds.status IN ('arrival', 'late')
AND al.check_in_at IS NOT NULL
AND al.check_out_at IS NULL
@@ -388,14 +389,18 @@ $$;
CREATE OR REPLACE FUNCTION public.enqueue_pass_slip_expiry_notifications()
RETURNS void LANGUAGE plpgsql AS $$
BEGIN
-- Match any active pass slip in the last 15 minutes (between 4560 min old).
-- Unlike narrow ±90s windows used for shift reminders, pass slips use a
-- 15-minute window so this function doesn't need to be called at an exact
-- second. ON CONFLICT DO NOTHING prevents duplicate notifications.
INSERT INTO public.scheduled_notifications (pass_slip_id, user_id, notify_type, scheduled_for)
SELECT ps.id, ps.user_id, 'pass_slip_expiry_15', now()
FROM public.pass_slips ps
WHERE ps.status = 'approved'
AND ps.slip_end IS NULL
AND ps.slip_start IS NOT NULL
AND ps.slip_start + interval '45 minutes'
BETWEEN now() - interval '90 seconds' AND now() + interval '90 seconds'
AND ps.slip_start + interval '45 minutes' <= now()
AND ps.slip_start + interval '60 minutes' > now()
ON CONFLICT DO NOTHING;
END;
$$;
@@ -420,8 +425,18 @@ $$;
-- ============================================================================
-- 4. PG_NET + PG_CRON SCHEDULING
-- ============================================================================
-- Uses pg_net to call the process_scheduled_notifications edge function
-- from within PostgreSQL, eliminating the need for external cron jobs.
-- The edge function (process_scheduled_notifications) now calls
-- enqueue_all_notifications() via RPC internally, so only ONE pg_cron job
-- is needed to trigger the edge function. This eliminates the fragile
-- dependency on two independent cron jobs both working.
--
-- Credential lookup order:
-- 1. vault.decrypted_secrets (Supabase cloud + self-hosted)
-- 2. app.settings GUC variables (self-hosted fallback)
--
-- ONE-TIME SETUP (run once after migration):
-- SELECT vault.create_secret('https://YOUR_PROJECT.supabase.co', 'supabase_url');
-- SELECT vault.create_secret('YOUR_SERVICE_ROLE_KEY', 'service_role_key');
CREATE EXTENSION IF NOT EXISTS pg_net;
@@ -432,7 +447,7 @@ DECLARE
v_base_url text;
v_service_key text;
BEGIN
-- Try Supabase vault first (standard in self-hosted Supabase)
-- Try Supabase vault first (works on both cloud and self-hosted)
BEGIN
SELECT decrypted_secret INTO v_service_key
FROM vault.decrypted_secrets
@@ -448,7 +463,7 @@ BEGIN
NULL;
END;
-- Fall back to app.settings GUC variables
-- Fall back to app.settings GUC variables (self-hosted)
IF v_base_url IS NULL THEN
v_base_url := current_setting('app.settings.supabase_url', true);
END IF;
@@ -458,10 +473,11 @@ BEGIN
-- Guard: skip if config is missing
IF v_base_url IS NULL OR v_service_key IS NULL THEN
RAISE WARNING 'process_notification_queue: missing supabase_url or service_role_key config';
RAISE WARNING 'process_notification_queue: missing vault secrets. Run: SELECT vault.create_secret(''https://YOUR_PROJECT.supabase.co'', ''supabase_url''); SELECT vault.create_secret(''YOUR_KEY'', ''service_role_key'');';
RETURN;
END IF;
-- The edge function handles both enqueue + process internally
PERFORM net.http_post(
url := v_base_url || '/functions/v1/process_scheduled_notifications',
headers := jsonb_build_object(
@@ -485,31 +501,25 @@ BEGIN
PERFORM cron.unschedule('notification_reminders_every_min');
EXCEPTION WHEN others THEN NULL;
END;
BEGIN
PERFORM cron.unschedule('notification_enqueue_every_min');
EXCEPTION WHEN others THEN NULL;
END;
-- Job 1: Enqueue notifications every minute
PERFORM cron.schedule(
'notification_enqueue_every_min',
'*/1 * * * *',
'SELECT public.enqueue_all_notifications();'
);
-- Job 2: Process notification queue via pg_net every minute
-- Single job: triggers edge function which handles enqueue + process
PERFORM cron.schedule(
'notification_process_every_min',
'*/1 * * * *',
'SELECT public.process_notification_queue();'
);
-- Job 3: Daily cleanup of old processed notifications
-- Daily cleanup of old processed notifications
PERFORM cron.schedule(
'cleanup_old_notifications',
'0 3 * * *',
'DELETE FROM scheduled_notifications WHERE processed = true AND processed_at < now() - interval ' || quote_literal('7 days') || '; DELETE FROM notification_pushes WHERE pushed_at < now() - interval ' || quote_literal('7 days') || ';'
'DELETE FROM scheduled_notifications WHERE processed = true AND processed_at < now() - interval ''7 days''; DELETE FROM notification_pushes WHERE pushed_at < now() - interval ''7 days'';'
);
EXCEPTION WHEN others THEN
RAISE NOTICE 'pg_cron/pg_net not available. After enabling them, run these manually:';
RAISE NOTICE ' SELECT cron.schedule(%L, %L, %L);', 'notification_enqueue_every_min', '*/1 * * * *', 'SELECT public.enqueue_all_notifications();';
RAISE NOTICE ' SELECT cron.schedule(%L, %L, %L);', 'notification_process_every_min', '*/1 * * * *', 'SELECT public.process_notification_queue();';
RAISE NOTICE ' SELECT cron.schedule(%L, %L, %L);', 'cleanup_old_notifications', '0 3 * * *', 'DELETE FROM scheduled_notifications WHERE processed = true AND processed_at < now() - interval ' || quote_literal('7 days') || '; DELETE FROM notification_pushes WHERE pushed_at < now() - interval ' || quote_literal('7 days') || ';';
RAISE NOTICE 'pg_cron scheduling failed: %. Set up cron jobs manually or use external cron.', SQLERRM;
END $$;