-- Migration: Fix enum literal validation errors in enqueue functions -- -- Problem: PostgreSQL validates enum literals against the enum type at -- execution time, even when no rows match. If a status value like 'closed' -- or 'in_progress_dry_run' was added to the enum in a later migration that -- hasn't been applied to a given deployment (e.g. self-hosted Docker), the -- entire enqueue_all_notifications() call aborts with: -- ERROR 22P02: invalid input value for enum : "" -- This prevents ALL notification types from being enqueued, not just the one -- that references the missing value. -- -- Fix: cast the enum column to ::text before comparing. Text comparison -- bypasses enum validation entirely and works regardless of which enum values -- exist on any given deployment. -- -- Affected functions and the risky literals: -- enqueue_overtime_checkout_notifications → task_status 'closed' -- enqueue_overtime_idle_notifications → it_service_request_status 'in_progress_dry_run' -- enqueue_isr_event_notifications → it_service_request_status 'in_progress_dry_run' -- enqueue_isr_evidence_notifications → it_service_request_status 'in_progress' (safe but cast for consistency) CREATE OR REPLACE FUNCTION public.enqueue_overtime_checkout_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; v_last_completed timestamptz; BEGIN FOR rec IN SELECT ds.id AS schedule_id, ds.user_id, al.check_in_at 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.shift_type = 'overtime' AND ds.status IN ('arrival', 'late') AND al.check_in_at IS NOT NULL AND al.check_out_at IS NULL AND al.check_in_at <= now() - interval '1 hour' AND NOT EXISTS ( SELECT 1 FROM public.task_assignments ta JOIN public.tasks t ON t.id = ta.task_id WHERE ta.user_id = ds.user_id AND t.status::text = 'in_progress' ) LOOP SELECT MAX(t.completed_at) INTO v_last_completed FROM public.task_assignments ta JOIN public.tasks t ON t.id = ta.task_id WHERE ta.user_id = rec.user_id AND t.status::text IN ('completed', 'closed') AND t.completed_at IS NOT NULL AND t.completed_at >= rec.check_in_at; IF v_last_completed IS NOT NULL AND v_last_completed <= now() - interval '30 minutes' THEN INSERT INTO public.scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'overtime_checkout_30', now()) ON CONFLICT DO NOTHING; END IF; END LOOP; END; $$; CREATE OR REPLACE FUNCTION public.enqueue_overtime_idle_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; BEGIN FOR rec IN SELECT ds.id AS schedule_id, ds.user_id 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.shift_type = 'overtime' AND ds.status IN ('arrival', 'late') AND al.check_in_at IS NOT NULL AND al.check_out_at IS NULL AND al.check_in_at <= now() - interval '15 minutes' AND NOT EXISTS ( SELECT 1 FROM public.task_assignments ta JOIN public.tasks t ON t.id = ta.task_id WHERE ta.user_id = ds.user_id AND t.status::text = 'in_progress' ) AND NOT EXISTS ( SELECT 1 FROM public.it_service_request_assignments isra JOIN public.it_service_requests isr ON isr.id = isra.request_id WHERE isra.user_id = ds.user_id AND isr.status::text IN ('in_progress', 'in_progress_dry_run') ) LOOP INSERT INTO public.scheduled_notifications (schedule_id, user_id, notify_type, scheduled_for) VALUES (rec.schedule_id, rec.user_id, 'overtime_idle_15', now()) ON CONFLICT DO NOTHING; END LOOP; END; $$; CREATE OR REPLACE FUNCTION public.enqueue_isr_event_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; BEGIN FOR rec IN SELECT isr.id AS request_id, isr.event_date, isra.user_id FROM public.it_service_requests isr JOIN public.it_service_request_assignments isra ON isra.request_id = isr.id WHERE isr.status::text IN ('scheduled', 'in_progress_dry_run') AND isr.event_date IS NOT NULL AND isr.event_date BETWEEN now() + interval '60 minutes' - interval '90 seconds' AND now() + interval '60 minutes' + interval '90 seconds' LOOP INSERT INTO public.scheduled_notifications (it_service_request_id, user_id, notify_type, scheduled_for) VALUES (rec.request_id, rec.user_id, 'isr_event_60', rec.event_date - interval '1 hour') ON CONFLICT DO NOTHING; END LOOP; END; $$; CREATE OR REPLACE FUNCTION public.enqueue_isr_evidence_notifications() RETURNS void LANGUAGE plpgsql AS $$ DECLARE rec RECORD; v_today_doy int := EXTRACT(DOY FROM now())::int; BEGIN FOR rec IN SELECT isr.id AS request_id, isra.user_id FROM public.it_service_requests isr JOIN public.it_service_request_assignments isra ON isra.request_id = isr.id WHERE isr.status::text IN ('completed', 'in_progress') AND ( NOT EXISTS ( SELECT 1 FROM public.it_service_request_evidence e WHERE e.request_id = isr.id AND e.user_id = isra.user_id ) OR NOT EXISTS ( SELECT 1 FROM public.it_service_request_actions a WHERE a.request_id = isr.id AND a.user_id = isra.user_id AND a.action_taken IS NOT NULL AND TRIM(a.action_taken) != '' ) ) AND EXISTS ( SELECT 1 FROM public.attendance_logs al WHERE al.user_id = isra.user_id AND al.check_in_at::date = now()::date ) LOOP INSERT INTO public.scheduled_notifications (it_service_request_id, user_id, notify_type, scheduled_for, epoch) VALUES (rec.request_id, rec.user_id, 'isr_evidence_daily', now(), v_today_doy) ON CONFLICT DO NOTHING; END LOOP; END; $$;