-- When a swap is accepted, any OTHER pending/admin_review swap requests that -- reference the same schedules become invalid (ownership changed). Rather than -- letting them linger as "pending" until someone taps them and gets a confusing -- ownership error, we automatically reject them in the same transaction. -- -- This fixes the "PM shift still showing after ON_CALL swap accepted" scenario: -- Swap A: User X's ON_CALL ↔ User Y's PM → accepted -- Swap B: User X's ON_CALL ↔ User Z's PM → auto-rejected here (stale) CREATE OR REPLACE FUNCTION public.respond_shift_swap(p_swap_id uuid, p_action text) RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_swap RECORD; BEGIN SELECT * INTO v_swap FROM public.swap_requests WHERE id = p_swap_id; IF NOT FOUND THEN RAISE EXCEPTION 'swap request not found'; END IF; IF p_action NOT IN ('accepted','rejected','admin_review') THEN RAISE EXCEPTION 'invalid action'; END IF; -- Idempotency guard: already in terminal state → nothing more to do. IF v_swap.status IN ('accepted', 'rejected') THEN RETURN; END IF; IF p_action = 'accepted' THEN -- only recipient or admin/dispatcher can accept IF NOT ( v_swap.recipient_id = auth.uid() OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')) ) THEN RAISE EXCEPTION 'permission denied'; END IF; -- ensure both shifts are still owned by the expected users before swapping IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.shift_id AND user_id = v_swap.requester_id) THEN RAISE EXCEPTION 'requester shift ownership changed, cannot accept swap'; END IF; IF v_swap.target_shift_id IS NULL THEN RAISE EXCEPTION 'target shift missing'; END IF; IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.target_shift_id AND user_id = v_swap.recipient_id) THEN RAISE EXCEPTION 'target shift ownership changed, cannot accept swap'; END IF; -- perform the swap (atomic within function) and stamp swap_request_id UPDATE public.duty_schedules SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id WHERE id = v_swap.shift_id; UPDATE public.duty_schedules SET user_id = v_swap.requester_id, swap_request_id = p_swap_id WHERE id = v_swap.target_shift_id; UPDATE public.swap_requests SET status = 'accepted', updated_at = now() WHERE id = p_swap_id; -- Auto-reject all OTHER pending/admin_review swap requests that reference -- either of the now-swapped schedules. These are stale — the shift -- ownerships changed, so they can never be fulfilled. UPDATE public.swap_requests SET status = 'rejected', updated_at = now() WHERE id <> p_swap_id AND status IN ('pending', 'admin_review') AND ( shift_id = v_swap.shift_id OR shift_id = v_swap.target_shift_id OR target_shift_id = v_swap.shift_id OR target_shift_id = v_swap.target_shift_id ); INSERT INTO public.swap_request_participants(swap_request_id, user_id, role) VALUES (p_swap_id, auth.uid(), 'approver') ON CONFLICT DO NOTHING; INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); ELSIF p_action = 'rejected' THEN -- only recipient or admin/dispatcher can reject IF NOT ( v_swap.recipient_id = auth.uid() OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher')) ) THEN RAISE EXCEPTION 'permission denied'; END IF; UPDATE public.swap_requests SET status = 'rejected', updated_at = now() WHERE id = p_swap_id; INSERT INTO public.swap_request_participants(swap_request_id, user_id, role) VALUES (p_swap_id, auth.uid(), 'approver') ON CONFLICT DO NOTHING; INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); ELSE -- admin_review -- only requester may escalate for admin review IF NOT (v_swap.requester_id = auth.uid()) THEN RAISE EXCEPTION 'permission denied'; END IF; UPDATE public.swap_requests SET status = 'admin_review', updated_at = now() WHERE id = p_swap_id; INSERT INTO public.notifications(user_id, actor_id, type, created_at) VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now()); END IF; END; $$; -- Re-grant EXECUTE (SECURITY DEFINER drops grants on replace). GRANT EXECUTE ON FUNCTION public.respond_shift_swap(uuid, text) TO authenticated;