134 lines
5.9 KiB
PL/PgSQL
134 lines
5.9 KiB
PL/PgSQL
-- Migration: Banner notification support for announcements
|
|
-- Adds banner_enabled, show/hide times, and scheduled push intervals.
|
|
-- Hooks into the existing scheduled_notifications queue + pg_cron pipeline.
|
|
|
|
-- ============================================================================
|
|
-- 1. Banner columns on announcements
|
|
-- ============================================================================
|
|
ALTER TABLE public.announcements
|
|
ADD COLUMN IF NOT EXISTS banner_enabled boolean NOT NULL DEFAULT false,
|
|
ADD COLUMN IF NOT EXISTS banner_show_at timestamptz, -- null = show immediately
|
|
ADD COLUMN IF NOT EXISTS banner_hide_at timestamptz, -- null = manual off only
|
|
ADD COLUMN IF NOT EXISTS push_interval_minutes integer; -- null = no scheduled push
|
|
|
|
-- Partial index: only index rows with active banners
|
|
CREATE INDEX IF NOT EXISTS idx_announcements_banner_active
|
|
ON public.announcements (banner_show_at, banner_hide_at)
|
|
WHERE banner_enabled = true AND push_interval_minutes IS NOT NULL;
|
|
|
|
-- ============================================================================
|
|
-- 2. Extend scheduled_notifications with announcement_id
|
|
-- ============================================================================
|
|
ALTER TABLE public.scheduled_notifications
|
|
ADD COLUMN IF NOT EXISTS announcement_id uuid
|
|
REFERENCES public.announcements(id) ON DELETE CASCADE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sched_notif_announcement
|
|
ON public.scheduled_notifications(announcement_id)
|
|
WHERE announcement_id IS NOT NULL;
|
|
|
|
-- 2a. Rebuild unique index to include announcement_id
|
|
DROP INDEX IF EXISTS uniq_sched_notif;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_sched_notif ON public.scheduled_notifications (
|
|
COALESCE(schedule_id, '00000000-0000-0000-0000-000000000000'),
|
|
COALESCE(task_id, '00000000-0000-0000-0000-000000000000'),
|
|
COALESCE(it_service_request_id, '00000000-0000-0000-0000-000000000000'),
|
|
COALESCE(pass_slip_id, '00000000-0000-0000-0000-000000000000'),
|
|
COALESCE(announcement_id, '00000000-0000-0000-0000-000000000000'),
|
|
user_id,
|
|
notify_type,
|
|
epoch
|
|
);
|
|
|
|
-- 2b. Update CHECK constraint to allow announcement-only rows
|
|
ALTER TABLE public.scheduled_notifications
|
|
DROP CONSTRAINT IF EXISTS chk_at_least_one_ref;
|
|
|
|
ALTER TABLE public.scheduled_notifications
|
|
ADD CONSTRAINT chk_at_least_one_ref CHECK (
|
|
schedule_id IS NOT NULL
|
|
OR task_id IS NOT NULL
|
|
OR it_service_request_id IS NOT NULL
|
|
OR pass_slip_id IS NOT NULL
|
|
OR announcement_id IS NOT NULL
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- 3. Enqueue function for banner announcement push notifications
|
|
-- ============================================================================
|
|
-- Called by enqueue_all_notifications() every minute via pg_cron.
|
|
-- For each active banner announcement with a push interval, inserts one
|
|
-- scheduled_notification per target user per interval epoch.
|
|
-- epoch = floor(unix_seconds / interval_seconds) ensures exactly one push
|
|
-- per user per interval window with ON CONFLICT DO NOTHING idempotency.
|
|
CREATE OR REPLACE FUNCTION public.enqueue_announcement_banner_notifications()
|
|
RETURNS void LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
ann RECORD;
|
|
usr RECORD;
|
|
v_epoch int;
|
|
BEGIN
|
|
FOR ann IN
|
|
SELECT a.id, a.author_id, a.visible_roles, a.push_interval_minutes
|
|
FROM public.announcements a
|
|
WHERE a.banner_enabled = true
|
|
AND a.push_interval_minutes IS NOT NULL
|
|
AND a.is_template = false
|
|
AND (a.banner_show_at IS NULL OR a.banner_show_at <= now())
|
|
AND (a.banner_hide_at IS NULL OR a.banner_hide_at > now())
|
|
LOOP
|
|
-- One epoch per interval window; prevents duplicate pushes within the window
|
|
v_epoch := FLOOR(
|
|
EXTRACT(EPOCH FROM now()) / (ann.push_interval_minutes * 60)
|
|
)::int;
|
|
|
|
-- Purge rows that are ≥2 epochs stale (unprocessed due to missed cycles).
|
|
-- We keep epoch = v_epoch - 1 because that row's scheduled_for falls
|
|
-- exactly at the current epoch boundary and is about to be processed.
|
|
-- Deleting epoch < v_epoch would remove rows that are due RIGHT NOW
|
|
-- (epoch E's row has scheduled_for = (E+1)*interval = now), causing
|
|
-- all notifications to stop firing.
|
|
DELETE FROM public.scheduled_notifications
|
|
WHERE announcement_id = ann.id
|
|
AND notify_type = 'announcement_banner'
|
|
AND processed = false
|
|
AND epoch < v_epoch - 1;
|
|
|
|
FOR usr IN
|
|
SELECT p.id
|
|
FROM public.profiles p
|
|
WHERE p.role::text = ANY(ann.visible_roles)
|
|
AND p.id <> ann.author_id
|
|
LOOP
|
|
INSERT INTO public.scheduled_notifications
|
|
(announcement_id, user_id, notify_type, scheduled_for, epoch)
|
|
VALUES
|
|
(ann.id, usr.id, 'announcement_banner',
|
|
to_timestamp(((v_epoch + 1)::bigint * ann.push_interval_minutes * 60)),
|
|
v_epoch)
|
|
ON CONFLICT DO NOTHING;
|
|
END LOOP;
|
|
END LOOP;
|
|
END;
|
|
$$;
|
|
|
|
-- ============================================================================
|
|
-- 4. Re-register master dispatcher to include banner announcements
|
|
-- ============================================================================
|
|
CREATE OR REPLACE FUNCTION public.enqueue_all_notifications()
|
|
RETURNS void LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
PERFORM public.enqueue_due_shift_notifications();
|
|
PERFORM public.enqueue_overtime_idle_notifications();
|
|
PERFORM public.enqueue_overtime_checkout_notifications();
|
|
PERFORM public.enqueue_isr_event_notifications();
|
|
PERFORM public.enqueue_isr_evidence_notifications();
|
|
PERFORM public.enqueue_paused_task_notifications();
|
|
PERFORM public.enqueue_backlog_notifications();
|
|
PERFORM public.enqueue_pass_slip_expiry_notifications();
|
|
PERFORM public.enqueue_pass_slip_expired_notifications(); -- added in 20260322150000; kept here
|
|
PERFORM public.enqueue_announcement_banner_notifications(); -- added in this migration
|
|
END;
|
|
$$;
|