-- ===================================================== -- 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;