228 lines
8.6 KiB
PL/PgSQL
228 lines
8.6 KiB
PL/PgSQL
-- Business rule: PM Duty always comes with ON_CALL (consecutive overnight
|
||
-- shift, same person, same calendar day). Accepting a swap that involves a
|
||
-- PM schedule must also atomically transfer the companion on_call duty
|
||
-- schedule to the new PM holder so the pairing stays intact.
|
||
--
|
||
-- Handles BOTH swap directions:
|
||
-- A) target_shift_id is PM → requester receives PM; transfer recipient's
|
||
-- companion ON_CALL to requester.
|
||
-- B) shift_id is PM → recipient receives PM; transfer requester's
|
||
-- companion ON_CALL to recipient.
|
||
--
|
||
-- Example (direction A — Normal user initiates):
|
||
-- Before: User A owns Normal (08:00–17:00), User B owns PM (15:00–23:00)
|
||
-- and ON_CALL (23:00–07:00) on the same calendar day.
|
||
-- After accept: User A owns PM + ON_CALL, User B owns Normal.
|
||
--
|
||
-- Example (direction B — PM user initiates):
|
||
-- Before: User B owns PM + ON_CALL, User A owns Normal.
|
||
-- After accept: same result — User A owns PM + ON_CALL, User B owns Normal.
|
||
--
|
||
-- Supersedes 20260322180000_auto_reject_orphaned_swaps.sql.
|
||
|
||
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;
|
||
v_target_type text;
|
||
v_target_date date;
|
||
v_requester_type text;
|
||
v_requester_date date;
|
||
v_companion_id uuid;
|
||
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 terminal → nothing to do.
|
||
IF v_swap.status IN ('accepted', 'rejected') THEN
|
||
RETURN;
|
||
END IF;
|
||
|
||
-- ── ACCEPTED ──────────────────────────────────────────────────────────────
|
||
IF p_action = 'accepted' THEN
|
||
|
||
-- Permission: recipient or admin/dispatcher
|
||
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;
|
||
|
||
-- Ownership validation 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;
|
||
|
||
-- Read both schedules' types and dates BEFORE the swap so we can decide
|
||
-- companion direction independently of ownership changes.
|
||
SELECT shift_type,
|
||
DATE(start_time AT TIME ZONE 'Asia/Manila')
|
||
INTO v_target_type, v_target_date
|
||
FROM public.duty_schedules
|
||
WHERE id = v_swap.target_shift_id;
|
||
|
||
SELECT shift_type,
|
||
DATE(start_time AT TIME ZONE 'Asia/Manila')
|
||
INTO v_requester_type, v_requester_date
|
||
FROM public.duty_schedules
|
||
WHERE id = v_swap.shift_id;
|
||
|
||
-- Primary swap: transfer both schedules atomically
|
||
UPDATE public.duty_schedules
|
||
SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id
|
||
WHERE id = v_swap.shift_id; -- requester's schedule → recipient
|
||
|
||
UPDATE public.duty_schedules
|
||
SET user_id = v_swap.requester_id, swap_request_id = p_swap_id
|
||
WHERE id = v_swap.target_shift_id; -- recipient's schedule → requester
|
||
|
||
-- ── Companion ON_CALL transfer ────────────────────────────────────────
|
||
-- Direction A: target is PM → requester is the new PM holder.
|
||
-- Find companion ON_CALL previously owned by the RECIPIENT and give it
|
||
-- to the REQUESTER.
|
||
IF v_target_type = 'pm' THEN
|
||
SELECT id INTO v_companion_id
|
||
FROM public.duty_schedules
|
||
WHERE user_id = v_swap.recipient_id
|
||
AND shift_type = 'on_call'
|
||
AND DATE(start_time AT TIME ZONE 'Asia/Manila') = v_target_date
|
||
ORDER BY start_time
|
||
LIMIT 1;
|
||
|
||
IF v_companion_id IS NOT NULL THEN
|
||
UPDATE public.duty_schedules
|
||
SET user_id = v_swap.requester_id, swap_request_id = p_swap_id
|
||
WHERE id = v_companion_id;
|
||
|
||
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_companion_id
|
||
OR target_shift_id = v_companion_id
|
||
);
|
||
END IF;
|
||
|
||
-- Direction B: requester's schedule is PM → recipient is the new PM holder.
|
||
-- Find companion ON_CALL previously owned by the REQUESTER and give it
|
||
-- to the RECIPIENT.
|
||
ELSIF v_requester_type = 'pm' THEN
|
||
SELECT id INTO v_companion_id
|
||
FROM public.duty_schedules
|
||
WHERE user_id = v_swap.requester_id
|
||
AND shift_type = 'on_call'
|
||
AND DATE(start_time AT TIME ZONE 'Asia/Manila') = v_requester_date
|
||
ORDER BY start_time
|
||
LIMIT 1;
|
||
|
||
IF v_companion_id IS NOT NULL THEN
|
||
UPDATE public.duty_schedules
|
||
SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id
|
||
WHERE id = v_companion_id;
|
||
|
||
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_companion_id
|
||
OR target_shift_id = v_companion_id
|
||
);
|
||
END IF;
|
||
END IF;
|
||
-- ─────────────────────────────────────────────────────────────────────
|
||
|
||
-- Accept the swap request
|
||
UPDATE public.swap_requests
|
||
SET status = 'accepted', updated_at = now()
|
||
WHERE id = p_swap_id;
|
||
|
||
-- Auto-reject all other pending swaps referencing either primary schedule
|
||
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());
|
||
|
||
-- ── REJECTED ──────────────────────────────────────────────────────────────
|
||
ELSIF p_action = 'rejected' THEN
|
||
|
||
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());
|
||
|
||
-- ── ADMIN_REVIEW ──────────────────────────────────────────────────────────
|
||
ELSE
|
||
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 after SECURITY DEFINER replace
|
||
GRANT EXECUTE ON FUNCTION public.respond_shift_swap(uuid, text) TO authenticated;
|