141 lines
4.3 KiB
PL/PgSQL
141 lines
4.3 KiB
PL/PgSQL
-- =====================================================
|
|
-- Atomic Event Registration
|
|
--
|
|
-- Problem: Registering for an event requires multiple
|
|
-- queries (check capacity, validate age, count registrations,
|
|
-- insert). Race conditions can over-register an event.
|
|
--
|
|
-- Fix:
|
|
-- A) Ensure member_id FK column exists on event_registrations
|
|
-- (idempotent — may already exist from 20260416000006).
|
|
-- B) Single transactional PG function that locks the event
|
|
-- row, validates capacity/age, and inserts with the
|
|
-- correct status (confirmed vs waitlisted).
|
|
-- =====================================================
|
|
|
|
-- A) Add member_id column if not already present
|
|
ALTER TABLE public.event_registrations
|
|
ADD COLUMN IF NOT EXISTS member_id uuid
|
|
REFERENCES public.members(id) ON DELETE SET NULL;
|
|
|
|
-- Ensure index exists (idempotent)
|
|
CREATE INDEX IF NOT EXISTS ix_event_registrations_member
|
|
ON public.event_registrations(member_id)
|
|
WHERE member_id IS NOT NULL;
|
|
|
|
-- The status CHECK constraint already includes 'waitlisted' in the
|
|
-- original schema: check (status in ('pending','confirmed','waitlisted','cancelled'))
|
|
-- No constraint modification needed.
|
|
|
|
-- B) Atomic registration function
|
|
CREATE OR REPLACE FUNCTION public.register_for_event(
|
|
p_event_id uuid,
|
|
p_member_id uuid DEFAULT NULL,
|
|
p_first_name text DEFAULT NULL,
|
|
p_last_name text DEFAULT NULL,
|
|
p_email text DEFAULT NULL,
|
|
p_phone text DEFAULT NULL,
|
|
p_date_of_birth date DEFAULT NULL,
|
|
p_parent_name text DEFAULT NULL,
|
|
p_parent_phone text DEFAULT NULL
|
|
)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
v_event record;
|
|
v_reg_count bigint;
|
|
v_status text;
|
|
v_age integer;
|
|
v_registration_id uuid;
|
|
BEGIN
|
|
-- 1. Lock the event row to prevent concurrent registration races
|
|
SELECT * INTO v_event
|
|
FROM public.events
|
|
WHERE id = p_event_id
|
|
FOR UPDATE;
|
|
|
|
IF v_event IS NULL THEN
|
|
RAISE EXCEPTION 'Event % not found', p_event_id
|
|
USING ERRCODE = 'P0002';
|
|
END IF;
|
|
|
|
-- 2. Validate event status is open for registration
|
|
IF v_event.status != 'open' THEN
|
|
RAISE EXCEPTION 'Event is not open for registration (current status: %)', v_event.status
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
-- 3. Check registration deadline hasn't passed
|
|
IF v_event.registration_deadline IS NOT NULL AND v_event.registration_deadline < current_date THEN
|
|
RAISE EXCEPTION 'Registration deadline (%) has passed', v_event.registration_deadline
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
-- 4. Age validation: calculate age at event_date if date_of_birth provided
|
|
IF p_date_of_birth IS NOT NULL THEN
|
|
v_age := extract(year FROM age(v_event.event_date, p_date_of_birth))::integer;
|
|
|
|
IF v_event.min_age IS NOT NULL AND v_age < v_event.min_age THEN
|
|
RAISE EXCEPTION 'Participant age (%) is below the minimum age (%) for this event', v_age, v_event.min_age
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
|
|
IF v_event.max_age IS NOT NULL AND v_age > v_event.max_age THEN
|
|
RAISE EXCEPTION 'Participant age (%) exceeds the maximum age (%) for this event', v_age, v_event.max_age
|
|
USING ERRCODE = 'P0001';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- 5. Count confirmed + pending registrations
|
|
SELECT count(*) INTO v_reg_count
|
|
FROM public.event_registrations
|
|
WHERE event_id = p_event_id
|
|
AND status IN ('confirmed', 'pending');
|
|
|
|
-- 6. Determine status based on capacity
|
|
IF v_event.capacity IS NOT NULL AND v_reg_count >= v_event.capacity THEN
|
|
v_status := 'waitlisted';
|
|
ELSE
|
|
v_status := 'confirmed';
|
|
END IF;
|
|
|
|
-- 7. Insert the registration
|
|
INSERT INTO public.event_registrations (
|
|
event_id,
|
|
member_id,
|
|
first_name,
|
|
last_name,
|
|
email,
|
|
phone,
|
|
date_of_birth,
|
|
parent_name,
|
|
parent_phone,
|
|
status
|
|
) VALUES (
|
|
p_event_id,
|
|
p_member_id,
|
|
p_first_name,
|
|
p_last_name,
|
|
p_email,
|
|
p_phone,
|
|
p_date_of_birth,
|
|
p_parent_name,
|
|
p_parent_phone,
|
|
v_status
|
|
)
|
|
RETURNING id INTO v_registration_id;
|
|
|
|
-- 8. Return result
|
|
RETURN jsonb_build_object(
|
|
'registration_id', v_registration_id,
|
|
'status', v_status
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.register_for_event(uuid, uuid, text, text, text, text, date, text, text) TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.register_for_event(uuid, uuid, text, text, text, text, date, text, text) TO service_role;
|