-- ===================================================== -- Atomic Booking Creation with Overlap Prevention -- -- Problem: Creating a booking requires checking room -- availability, validating capacity, and inserting — all -- as separate queries. Race conditions can double-book -- a room for overlapping dates. -- -- Fix: -- A) Enable btree_gist extension for exclusion constraints. -- B) Add GiST exclusion constraint to prevent overlapping -- bookings for the same room (non-cancelled/no_show). -- C) Single transactional PG function that locks the room, -- validates inputs, calculates price, and inserts. The -- exclusion constraint provides a final safety net. -- ===================================================== -- A) Enable btree_gist extension (required for exclusion constraints on non-GiST types) CREATE EXTENSION IF NOT EXISTS btree_gist; -- B) Add exclusion constraint to prevent overlapping bookings (idempotent) DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'excl_booking_room_dates' ) THEN ALTER TABLE public.bookings ADD CONSTRAINT excl_booking_room_dates EXCLUDE USING gist ( room_id WITH =, daterange(check_in, check_out) WITH && ) WHERE (status NOT IN ('cancelled', 'no_show')); END IF; END; $$; -- C) Atomic booking creation function CREATE OR REPLACE FUNCTION public.create_booking_atomic( p_account_id uuid, p_room_id uuid, p_guest_id uuid DEFAULT NULL, p_check_in date DEFAULT NULL, p_check_out date DEFAULT NULL, p_adults integer DEFAULT 1, p_children integer DEFAULT 0, p_status text DEFAULT 'confirmed', p_total_price numeric DEFAULT NULL, p_notes text DEFAULT NULL ) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_room record; v_computed_price numeric(10,2); v_booking_id uuid; BEGIN -- 1. Lock the room row to serialize booking attempts SELECT * INTO v_room FROM public.rooms WHERE id = p_room_id FOR UPDATE; -- 2. Validate room exists IF v_room IS NULL THEN RAISE EXCEPTION 'Room % not found', p_room_id USING ERRCODE = 'P0002'; END IF; -- 3. Validate check_out > check_in IF p_check_in IS NULL OR p_check_out IS NULL THEN RAISE EXCEPTION 'check_in and check_out dates are required' USING ERRCODE = 'P0001'; END IF; IF p_check_out <= p_check_in THEN RAISE EXCEPTION 'check_out (%) must be after check_in (%)', p_check_out, p_check_in USING ERRCODE = 'P0001'; END IF; -- 4. Validate total guests do not exceed room capacity IF (p_adults + p_children) > v_room.capacity THEN RAISE EXCEPTION 'Total guests (%) exceed room capacity (%)', (p_adults + p_children), v_room.capacity USING ERRCODE = 'P0001'; END IF; -- 5. Calculate price if not provided IF p_total_price IS NOT NULL THEN v_computed_price := p_total_price; ELSE v_computed_price := v_room.price_per_night * (p_check_out - p_check_in); END IF; -- 6. Insert the booking (exclusion constraint prevents double-booking) INSERT INTO public.bookings ( account_id, room_id, guest_id, check_in, check_out, adults, children, status, total_price, notes ) VALUES ( p_account_id, p_room_id, p_guest_id, p_check_in, p_check_out, p_adults, p_children, p_status, v_computed_price, p_notes ) RETURNING id INTO v_booking_id; -- 7. Return the new booking id RETURN v_booking_id; END; $$; GRANT EXECUTE ON FUNCTION public.create_booking_atomic(uuid, uuid, uuid, date, date, integer, integer, text, numeric, text) TO authenticated; GRANT EXECUTE ON FUNCTION public.create_booking_atomic(uuid, uuid, uuid, date, date, integer, integer, text, numeric, text) TO service_role;