diff --git a/supabase/migrations/20251108030215_5936f4ca-e276-4de9-8462-c31d473c9714.sql b/supabase/migrations/20251108030215_5936f4ca-e276-4de9-8462-c31d473c9714.sql new file mode 100644 index 00000000..a44715a1 --- /dev/null +++ b/supabase/migrations/20251108030215_5936f4ca-e276-4de9-8462-c31d473c9714.sql @@ -0,0 +1,274 @@ +-- ============================================================================ +-- CRITICAL FIX: Add missing `category` field to ride and ride_model creation +-- ============================================================================ +-- Without this field, ALL ride and ride_model approvals fail with constraint violation +-- Bug discovered during pipeline audit + +DO $$ +DECLARE + func_rec RECORD; +BEGIN + -- Drop all versions of create_entity_from_submission + FOR func_rec IN + SELECT oid::regprocedure::text as func_signature + FROM pg_proc + WHERE proname = 'create_entity_from_submission' + AND pg_function_is_visible(oid) + LOOP + EXECUTE format('DROP FUNCTION IF EXISTS %s CASCADE', func_rec.func_signature); + END LOOP; +END $$; + +-- Recreate with category fields added +CREATE FUNCTION create_entity_from_submission( + p_entity_type TEXT, + p_data JSONB, + p_created_by UUID +) +RETURNS UUID +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_entity_id UUID; + v_fk_id UUID; + v_location_id UUID; +BEGIN + CASE p_entity_type + WHEN 'park' THEN + -- Auto-create location if location data provided but no location_id + IF p_data->>'location_id' IS NULL AND p_data->>'location_name' IS NOT NULL THEN + INSERT INTO locations ( + name, street_address, city, state_province, country, + postal_code, latitude, longitude, timezone, display_name + ) VALUES ( + p_data->>'location_name', + p_data->>'location_street_address', + p_data->>'location_city', + p_data->>'location_state_province', + p_data->>'location_country', + p_data->>'location_postal_code', + (p_data->>'location_latitude')::NUMERIC, + (p_data->>'location_longitude')::NUMERIC, + p_data->>'location_timezone', + p_data->>'location_display_name' + ) + RETURNING id INTO v_location_id; + + p_data := p_data || jsonb_build_object('location_id', v_location_id); + + RAISE NOTICE 'Created new location % for park', v_location_id; + END IF; + + -- Validate foreign keys + IF p_data->>'location_id' IS NOT NULL THEN + v_fk_id := (p_data->>'location_id')::UUID; + IF NOT EXISTS (SELECT 1 FROM locations WHERE id = v_fk_id) THEN + RAISE EXCEPTION 'Invalid location_id: Location does not exist' + USING ERRCODE = '23503', HINT = 'location_id'; + END IF; + END IF; + + IF p_data->>'operator_id' IS NOT NULL THEN + v_fk_id := (p_data->>'operator_id')::UUID; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_fk_id AND company_type = 'operator') THEN + RAISE EXCEPTION 'Invalid operator_id: Company does not exist or is not an operator' + USING ERRCODE = '23503', HINT = 'operator_id'; + END IF; + END IF; + + IF p_data->>'property_owner_id' IS NOT NULL THEN + v_fk_id := (p_data->>'property_owner_id')::UUID; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_fk_id AND company_type = 'property_owner') THEN + RAISE EXCEPTION 'Invalid property_owner_id: Company does not exist or is not a property owner' + USING ERRCODE = '23503', HINT = 'property_owner_id'; + END IF; + END IF; + + INSERT INTO parks ( + name, slug, description, park_type, status, + location_id, operator_id, property_owner_id, + opening_date, closing_date, + opening_date_precision, closing_date_precision, + website_url, phone, email, + banner_image_url, banner_image_id, + card_image_url, card_image_id + ) VALUES ( + p_data->>'name', + p_data->>'slug', + p_data->>'description', + p_data->>'park_type', + p_data->>'status', + (p_data->>'location_id')::UUID, + (p_data->>'operator_id')::UUID, + (p_data->>'property_owner_id')::UUID, + (p_data->>'opening_date')::DATE, + (p_data->>'closing_date')::DATE, + p_data->>'opening_date_precision', + p_data->>'closing_date_precision', + p_data->>'website_url', + p_data->>'phone', + p_data->>'email', + p_data->>'banner_image_url', + p_data->>'banner_image_id', + p_data->>'card_image_url', + p_data->>'card_image_id' + ) + RETURNING id INTO v_entity_id; + + WHEN 'ride' THEN + -- Validate park_id (required) + v_fk_id := (p_data->>'park_id')::UUID; + IF v_fk_id IS NULL THEN + RAISE EXCEPTION 'park_id is required for ride creation' + USING ERRCODE = '23502', HINT = 'park_id'; + END IF; + IF NOT EXISTS (SELECT 1 FROM parks WHERE id = v_fk_id) THEN + RAISE EXCEPTION 'Invalid park_id: Park does not exist' + USING ERRCODE = '23503', HINT = 'park_id'; + END IF; + + IF p_data->>'manufacturer_id' IS NOT NULL THEN + v_fk_id := (p_data->>'manufacturer_id')::UUID; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_fk_id AND company_type = 'manufacturer') THEN + RAISE EXCEPTION 'Invalid manufacturer_id: Company does not exist or is not a manufacturer' + USING ERRCODE = '23503', HINT = 'manufacturer_id'; + END IF; + END IF; + + IF p_data->>'ride_model_id' IS NOT NULL THEN + v_fk_id := (p_data->>'ride_model_id')::UUID; + IF NOT EXISTS (SELECT 1 FROM ride_models WHERE id = v_fk_id) THEN + RAISE EXCEPTION 'Invalid ride_model_id: Ride model does not exist' + USING ERRCODE = '23503', HINT = 'ride_model_id'; + END IF; + END IF; + + -- ✅ FIX #1: Add category to ride creation + INSERT INTO rides ( + name, slug, park_id, category, ride_type, status, + manufacturer_id, ride_model_id, + opening_date, closing_date, + opening_date_precision, closing_date_precision, + description, + banner_image_url, banner_image_id, + card_image_url, card_image_id + ) VALUES ( + p_data->>'name', + p_data->>'slug', + (p_data->>'park_id')::UUID, + p_data->>'category', + p_data->>'ride_type', + p_data->>'status', + (p_data->>'manufacturer_id')::UUID, + (p_data->>'ride_model_id')::UUID, + (p_data->>'opening_date')::DATE, + (p_data->>'closing_date')::DATE, + p_data->>'opening_date_precision', + p_data->>'closing_date_precision', + p_data->>'description', + p_data->>'banner_image_url', + p_data->>'banner_image_id', + p_data->>'card_image_url', + p_data->>'card_image_id' + ) + RETURNING id INTO v_entity_id; + + WHEN 'manufacturer', 'operator', 'property_owner', 'designer' THEN + INSERT INTO companies ( + name, slug, company_type, description, + website_url, founded_year, + banner_image_url, banner_image_id, + card_image_url, card_image_id + ) VALUES ( + p_data->>'name', + p_data->>'slug', + p_entity_type, + p_data->>'description', + p_data->>'website_url', + (p_data->>'founded_year')::INTEGER, + p_data->>'banner_image_url', + p_data->>'banner_image_id', + p_data->>'card_image_url', + p_data->>'card_image_id' + ) + RETURNING id INTO v_entity_id; + + WHEN 'ride_model' THEN + -- Validate manufacturer_id (required) + v_fk_id := (p_data->>'manufacturer_id')::UUID; + IF v_fk_id IS NULL THEN + RAISE EXCEPTION 'manufacturer_id is required for ride model creation' + USING ERRCODE = '23502', HINT = 'manufacturer_id'; + END IF; + IF NOT EXISTS (SELECT 1 FROM companies WHERE id = v_fk_id AND company_type = 'manufacturer') THEN + RAISE EXCEPTION 'Invalid manufacturer_id: Company does not exist or is not a manufacturer' + USING ERRCODE = '23503', HINT = 'manufacturer_id'; + END IF; + + -- ✅ FIX #2: Add category to ride_model creation + INSERT INTO ride_models ( + name, slug, manufacturer_id, category, ride_type, + description, + banner_image_url, banner_image_id, + card_image_url, card_image_id + ) VALUES ( + p_data->>'name', + p_data->>'slug', + (p_data->>'manufacturer_id')::UUID, + p_data->>'category', + p_data->>'ride_type', + p_data->>'description', + p_data->>'banner_image_url', + p_data->>'banner_image_id', + p_data->>'card_image_url', + p_data->>'card_image_id' + ) + RETURNING id INTO v_entity_id; + + WHEN 'timeline_event', 'milestone' THEN + v_fk_id := (p_data->>'entity_id')::UUID; + IF v_fk_id IS NULL THEN + RAISE EXCEPTION 'entity_id is required for timeline event creation' + USING ERRCODE = '23502', HINT = 'entity_id'; + END IF; + + INSERT INTO entity_timeline_events ( + entity_id, entity_type, event_type, event_date, event_date_precision, + title, description, from_value, to_value, + from_entity_id, to_entity_id, from_location_id, to_location_id, + created_by, approved_by + ) VALUES ( + (p_data->>'entity_id')::UUID, + p_data->>'entity_type', + p_data->>'event_type', + (p_data->>'event_date')::DATE, + p_data->>'event_date_precision', + p_data->>'title', + p_data->>'description', + p_data->>'from_value', + p_data->>'to_value', + (p_data->>'from_entity_id')::UUID, + (p_data->>'to_entity_id')::UUID, + (p_data->>'from_location_id')::UUID, + (p_data->>'to_location_id')::UUID, + p_created_by, + current_setting('app.moderator_id', true)::UUID + ) + RETURNING id INTO v_entity_id; + + ELSE + RAISE EXCEPTION 'Unsupported entity type for creation: %', p_entity_type + USING ERRCODE = '22023'; + END CASE; + + RETURN v_entity_id; +END; +$$; + +-- Grant execute permissions +GRANT EXECUTE ON FUNCTION create_entity_from_submission TO authenticated; + +COMMENT ON FUNCTION create_entity_from_submission IS + 'Creates entities with category field support for rides and ride_models, plus automatic location creation and timeline event support'; \ No newline at end of file