diff --git a/docs/TEST_DATA_GENERATOR.md b/docs/TEST_DATA_GENERATOR.md index 69e9c424..c403fe54 100644 --- a/docs/TEST_DATA_GENERATOR.md +++ b/docs/TEST_DATA_GENERATOR.md @@ -23,37 +23,67 @@ The Test Data Generator is a comprehensive testing utility that creates realisti ### Presets -#### Small (~20 submissions) +#### Small (~30 submissions) - **Use Case**: Quick sanity checks, basic functionality testing -- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models -- **Time**: ~2-5 seconds +- **Contents**: 5 parks, 10 rides, 3 companies, 2 ride models, 5 photo sets +- **Features**: Mixed field density, photo support +- **Time**: ~3-7 seconds -#### Medium (~100 submissions) +#### Medium (~125 submissions) - **Use Case**: Standard testing, queue management validation -- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models -- **Time**: ~10-20 seconds +- **Contents**: 20 parks, 50 rides, 20 companies, 10 ride models, 25 photo sets +- **Features**: Full field variation, technical data, photos +- **Time**: ~15-30 seconds -#### Large (~500 submissions) +#### Large (~600 submissions) - **Use Case**: Performance testing, pagination verification -- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models -- **Time**: ~45-90 seconds +- **Contents**: 100 parks, 250 rides, 100 companies, 50 ride models, 100 photo sets +- **Features**: Complete field population, stats, specs, former names +- **Time**: ~60-120 seconds -#### Stress (~2000 submissions) +#### Stress (~2600 submissions) - **Use Case**: Load testing, database performance -- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models -- **Time**: ~3-5 minutes +- **Contents**: 400 parks, 1000 rides, 400 companies, 200 ride models, 500 photo sets +- **Features**: Maximum data density, all technical data, hundreds of photos +- **Time**: ~4-7 minutes ### Entity Types Select which entity types to generate: -- **Parks**: Theme parks, amusement parks, water parks -- **Rides**: Roller coasters, flat rides, water rides, dark rides +- **Parks**: Theme parks, amusement parks, water parks (with locations, operators, property owners) +- **Rides**: Roller coasters, flat rides, water rides, dark rides (with technical specs, coaster stats, former names) - **Manufacturers**: Companies that build rides - **Operators**: Companies that operate parks - **Property Owners**: Companies that own park properties - **Designers**: Individuals/companies that design rides - **Ride Models**: Specific ride model types from manufacturers +- **Photos**: Photo submissions with 1-10 photos each, captions, metadata + +### Field Population Density + +Control how many optional fields are populated: + +#### Mixed (Recommended) +- **Distribution**: 10% minimal, 20% basic, 40% standard, 20% complete, 10% maximum +- **Most Realistic**: Matches real-world usage patterns +- **Tests**: All levels of data completeness + +#### Minimal +- **Fields**: Required fields only +- **Use**: Test minimum viable submissions +- **Performance**: Fastest generation + +#### Standard +- **Fields**: Required + 50% optional +- **Use**: Balanced testing scenario +- **Performance**: Moderate generation time + +#### Maximum +- **Fields**: All fields + technical data +- **Includes**: Coaster stats, technical specs, former names +- **Use**: Complete data testing +- **Performance**: Slowest generation ### Advanced Options diff --git a/src/components/admin/TestDataGenerator.tsx b/src/components/admin/TestDataGenerator.tsx index 9a4d9acd..3c775045 100644 --- a/src/components/admin/TestDataGenerator.tsx +++ b/src/components/admin/TestDataGenerator.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; @@ -10,19 +10,20 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/component import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; -import { Beaker, CheckCircle, XCircle, ChevronDown, Trash2, AlertTriangle } from 'lucide-react'; +import { Beaker, CheckCircle, ChevronDown, Trash2, AlertTriangle } from 'lucide-react'; import { clearTestData, getTestDataStats } from '@/lib/testDataGenerator'; const PRESETS = { - small: { label: 'Small', description: '~20 submissions - Quick test', counts: '5 parks, 10 rides, 3 companies' }, - medium: { label: 'Medium', description: '~100 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies' }, - large: { label: 'Large', description: '~500 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies' }, - stress: { label: 'Stress', description: '~2000 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies' } + small: { label: 'Small', description: '~30 submissions - Quick test', counts: '5 parks, 10 rides, 3 companies, 2 models, 5 photo sets' }, + medium: { label: 'Medium', description: '~125 submissions - Standard testing', counts: '20 parks, 50 rides, 20 companies, 10 models, 25 photo sets' }, + large: { label: 'Large', description: '~600 submissions - Performance testing', counts: '100 parks, 250 rides, 100 companies, 50 models, 100 photo sets' }, + stress: { label: 'Stress', description: '~2600 submissions - Load testing', counts: '400 parks, 1000 rides, 400 companies, 200 models, 500 photo sets' } }; export function TestDataGenerator() { const { toast } = useToast(); const [preset, setPreset] = useState<'small' | 'medium' | 'large' | 'stress'>('small'); + const [fieldDensity, setFieldDensity] = useState<'mixed' | 'minimal' | 'standard' | 'maximum'>('mixed'); const [entityTypes, setEntityTypes] = useState({ parks: true, rides: true, @@ -30,7 +31,8 @@ export function TestDataGenerator() { operators: true, property_owners: true, designers: true, - ride_models: true + ride_models: true, + photos: true }); const [options, setOptions] = useState({ includeDependencies: true, @@ -47,6 +49,10 @@ export function TestDataGenerator() { .filter(([_, enabled]) => enabled) .map(([type]) => type); + useEffect(() => { + loadStats(); + }, []); + const loadStats = async () => { try { const data = await getTestDataStats(); @@ -64,6 +70,7 @@ export function TestDataGenerator() { const { data, error } = await supabase.functions.invoke('seed-test-data', { body: { preset, + fieldDensity, entityTypes: selectedEntityTypes, ...options } @@ -76,7 +83,7 @@ export function TestDataGenerator() { toast({ title: 'Test Data Generated', - description: `Successfully created ${Object.values(data.summary).reduce((a: number, b: number) => a + b, 0)} submissions in ${data.time}s` + description: `Successfully created test data in ${data.time}s` }); } catch (error) { console.error('Generation error:', error); @@ -122,7 +129,7 @@ export function TestDataGenerator() { Test Data Generator - Generate realistic test submissions for testing the moderation queue and versioning systems + Generate comprehensive test submissions with varying field density and photo support @@ -158,6 +165,39 @@ export function TestDataGenerator() { +
+ +

+ Controls how many optional fields are populated in generated entities +

+ setFieldDensity(v)} className="space-y-2"> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
@@ -219,6 +259,9 @@ export function TestDataGenerator() {
  • • Created {results.summary.rides} ride submissions
  • • Created {results.summary.companies} company submissions
  • • Created {results.summary.rideModels} ride model submissions
  • + {results.summary.photos > 0 && ( +
  • • Created {results.summary.photos} photo submissions ({results.summary.totalPhotoItems || 0} photos)
  • + )}
  • Time taken: {results.time}s
  • diff --git a/src/lib/testDataGenerator.ts b/src/lib/testDataGenerator.ts index 2c138f15..5bd91bde 100644 --- a/src/lib/testDataGenerator.ts +++ b/src/lib/testDataGenerator.ts @@ -3,10 +3,10 @@ import type { ParkSubmissionData, RideSubmissionData, CompanySubmissionData, Rid // Preset configurations export const PRESETS = { - small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 0 }, - medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 0 }, - large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 0 }, - stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 0 } + small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 }, + medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 }, + large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 }, + stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 500 } } as const; // Word lists for realistic names diff --git a/supabase/functions/seed-test-data/index.ts b/supabase/functions/seed-test-data/index.ts index 77db4572..9966e56b 100644 --- a/supabase/functions/seed-test-data/index.ts +++ b/supabase/functions/seed-test-data/index.ts @@ -8,11 +8,12 @@ const corsHeaders = { interface SeedOptions { preset: 'small' | 'medium' | 'large' | 'stress'; entityTypes: string[]; - includeDependencies: boolean; - includeConflicts: boolean; - includeVersionChains: boolean; - includeEscalated: boolean; - includeExpiredLocks: boolean; + fieldDensity?: 'mixed' | 'minimal' | 'standard' | 'maximum'; + includeDependencies?: boolean; + includeConflicts?: boolean; + includeVersionChains?: boolean; + includeEscalated?: boolean; + includeExpiredLocks?: boolean; } interface SeedPlan { @@ -20,33 +21,65 @@ interface SeedPlan { rides: number; companies: number; rideModels: number; + photos: number; } const PRESETS: Record = { - small: { parks: 5, rides: 10, companies: 3, rideModels: 2 }, - medium: { parks: 20, rides: 50, companies: 20, rideModels: 10 }, - large: { parks: 100, rides: 250, companies: 100, rideModels: 50 }, - stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200 } + small: { parks: 5, rides: 10, companies: 3, rideModels: 2, photos: 5 }, + medium: { parks: 20, rides: 50, companies: 20, rideModels: 10, photos: 25 }, + large: { parks: 100, rides: 250, companies: 100, rideModels: 50, photos: 100 }, + stress: { parks: 400, rides: 1000, companies: 400, rideModels: 200, photos: 500 } }; +const CITIES = [ + { city: 'Orlando', state: 'Florida', country: 'USA' }, + { city: 'Anaheim', state: 'California', country: 'USA' }, + { city: 'Paris', state: 'Île-de-France', country: 'France' }, + { city: 'Tokyo', state: 'Tokyo', country: 'Japan' }, + { city: 'Berlin', state: 'Berlin', country: 'Germany' } +]; + +// Helper functions +function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function randomItem(array: T[]): T { + return array[randomInt(0, array.length - 1)]; +} + +function randomDate(startYear: number, endYear: number): string { + const year = randomInt(startYear, endYear); + const month = randomInt(1, 12); + const day = randomInt(1, 28); + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; +} + +function getPopulationLevel(fieldDensity: string, index: number): number { + if (fieldDensity === 'mixed') { + const rand = Math.random(); + if (rand < 0.1) return 0; // 10% minimal + if (rand < 0.3) return 1; // 20% basic + if (rand < 0.7) return 2; // 40% standard + if (rand < 0.9) return 3; // 20% complete + return 4; // 10% maximum + } + if (fieldDensity === 'minimal') return 0; + if (fieldDensity === 'standard') return 2; + if (fieldDensity === 'maximum') return 4; + return 2; +} + Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { - // SECURITY: Service Role Key Usage - // --------------------------------- - // This function uses the service role key to seed test data bypassing RLS. - // This is required because: - // 1. Test data generation needs to create entities in protected tables - // 2. Moderator role is verified via is_moderator() RPC call before proceeding - // Scope: Limited to moderators only, for test/development purposes const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get auth header const authHeader = req.headers.get('Authorization'); if (!authHeader) { return new Response(JSON.stringify({ error: 'No authorization header' }), { @@ -55,7 +88,6 @@ Deno.serve(async (req) => { }); } - // Verify user is moderator const token = authHeader.replace('Bearer ', ''); const { data: { user }, error: userError } = await supabase.auth.getUser(token); @@ -67,25 +99,25 @@ Deno.serve(async (req) => { } const { data: isMod, error: modError } = await supabase.rpc('is_moderator', { _user_id: user.id }); - if (modError) { - console.error('Failed to check moderator status:', modError); - return new Response(JSON.stringify({ error: 'Failed to verify permissions' }), { - status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } - }); - } - - if (!isMod) { + if (modError || !isMod) { return new Response(JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }), { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }); } - // Parse request - const { preset = 'small', entityTypes = [], includeDependencies = true, includeConflicts = false, includeVersionChains = false, includeEscalated = false, includeExpiredLocks = false }: SeedOptions = await req.json(); + const { + preset = 'small', + entityTypes = [], + fieldDensity = 'mixed', + includeDependencies = true, + includeConflicts = false, + includeVersionChains = false, + includeEscalated = false, + includeExpiredLocks = false + }: SeedOptions = await req.json(); + const plan = PRESETS[preset]; - if (!plan) { return new Response(JSON.stringify({ error: 'Invalid preset' }), { status: 400, @@ -94,7 +126,7 @@ Deno.serve(async (req) => { } const startTime = Date.now(); - const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0, conflicts: 0, versionChains: 0 }; + const summary = { parks: 0, rides: 0, companies: 0, rideModels: 0, photos: 0, totalPhotoItems: 0, conflicts: 0, versionChains: 0 }; const createdParks: string[] = []; const createdCompanies: Record = { manufacturer: [], operator: [], designer: [], property_owner: [] }; const createdParkSlugs: string[] = []; @@ -102,11 +134,6 @@ Deno.serve(async (req) => { // Helper to create submission async function createSubmission(userId: string, type: string, itemData: any, options: { escalated?: boolean; expiredLock?: boolean } = {}) { - // Ensure crypto.randomUUID is available - if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function') { - throw new Error('crypto.randomUUID is not available in this environment'); - } - const submissionId = crypto.randomUUID(); const itemId = crypto.randomUUID(); @@ -115,12 +142,12 @@ Deno.serve(async (req) => { metadata: { is_test_data: true, generated_at: new Date().toISOString(), - generator_version: '1.0.0', - preset + generator_version: '2.0.0', + preset, + fieldDensity } }; - // Create content_submission const submissionData: any = { id: submissionId, user_id: userId, @@ -128,40 +155,32 @@ Deno.serve(async (req) => { status: 'pending', content: contentData, submitted_at: new Date().toISOString(), - priority: options.escalated ? 10 : Math.floor(Math.random() * 5) + 1 + priority: options.escalated ? 10 : randomInt(1, 5) }; if (options.escalated) { submissionData.escalated = true; - submissionData.escalation_reason = 'Test escalation'; + submissionData.escalation_reason = 'Test escalation for priority testing'; } if (options.expiredLock) { submissionData.assigned_to = userId; - submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); // 30 min ago + submissionData.locked_until = new Date(Date.now() - 1000 * 60 * 30).toISOString(); } - const { error: subError } = await supabase - .from('content_submissions') - .insert(submissionData); - + const { error: subError } = await supabase.from('content_submissions').insert(submissionData); if (subError) throw subError; - // Create submission_item - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - id: itemId, - submission_id: submissionId, - item_type: type, - item_data: itemData, - status: 'pending', - order_index: 0 - }); - + const { error: itemError } = await supabase.from('submission_items').insert({ + id: itemId, + submission_id: submissionId, + item_type: type, + item_data: itemData, + status: 'pending', + order_index: 0 + }); if (itemError) throw itemError; - // Create type-specific submission record const typeTableMap: Record = { park: 'park_submissions', ride: 'ride_submissions', @@ -179,43 +198,68 @@ Deno.serve(async (req) => { typeData.company_type = type; } - const { error: typeError } = await supabase - .from(table) - .insert(typeData); - + const { data: insertedData, error: typeError } = await supabase.from(table).insert(typeData).select('id').single(); if (typeError) throw typeError; + return { submissionId, typeId: insertedData?.id }; } - return submissionId; + return { submissionId, typeId: null }; } // Create parks if (entityTypes.includes('parks')) { for (let i = 0; i < plan.parks; i++) { - // Determine if this should be a conflict or version chain + const level = getPopulationLevel(fieldDensity, i); const shouldConflict = includeConflicts && createdParkSlugs.length > 0 && Math.random() < 0.15; const shouldVersionChain = includeVersionChains && createdParkSlugs.length > 0 && Math.random() < 0.15; let slug = `test-park-${i + 1}`; if (shouldConflict) { - // Reuse an existing slug to create a conflict - slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)]; + slug = randomItem(createdParkSlugs); summary.conflicts++; } else if (shouldVersionChain) { - // Reuse an existing slug for a version chain with different data - slug = createdParkSlugs[Math.floor(Math.random() * createdParkSlugs.length)]; + slug = randomItem(createdParkSlugs); summary.versionChains++; } - const parkData = { + const parkData: any = { name: shouldVersionChain ? `Test Park ${slug} (Updated)` : `Test Park ${i + 1}`, slug: slug, - description: shouldVersionChain ? 'Updated test park description' : 'Test park description', - park_type: ['theme_park', 'amusement_park', 'water_park'][Math.floor(Math.random() * 3)], - status: 'operating', - opening_date: '2000-01-01' + park_type: randomItem(['theme_park', 'amusement_park', 'water_park']), + status: 'operating' }; + if (level >= 1) { + parkData.opening_date = randomDate(1950, 2024); + parkData.description = `A ${parkData.park_type === 'theme_park' ? 'themed' : 'exciting'} park for all ages with various attractions.`; + } + + if (level >= 2) { + parkData.website_url = `https://test-park-${i + 1}.example.com`; + parkData.phone = `+1-555-${randomInt(100, 999)}-${randomInt(1000, 9999)}`; + } + + if (level >= 3 && createdCompanies.operator.length > 0) { + const { data: operatorData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.operator)).maybeSingle(); + if (operatorData) parkData.operator_id = operatorData.id; + parkData.email = `info@test-park-${i + 1}.example.com`; + parkData.card_image_id = `test-park-card-${i + 1}`; + parkData.card_image_url = `https://imagedelivery.net/test/park-${i + 1}/card`; + } + + if (level >= 4) { + if (createdCompanies.property_owner.length > 0) { + const { data: ownerData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.property_owner)).maybeSingle(); + if (ownerData) parkData.property_owner_id = ownerData.id; + } + if (Math.random() > 0.9) { + parkData.closing_date = randomDate(2000, 2024); + parkData.status = 'closed'; + } + parkData.banner_image_id = `test-park-banner-${i + 1}`; + parkData.banner_image_url = `https://imagedelivery.net/test/park-${i + 1}/banner`; + } + const options = { escalated: includeEscalated && Math.random() < 0.1, expiredLock: includeExpiredLocks && Math.random() < 0.1 @@ -233,18 +277,39 @@ Deno.serve(async (req) => { // Create companies const companyTypes = ['manufacturer', 'operator', 'designer', 'property_owner']; for (const compType of companyTypes) { - if (entityTypes.includes(compType)) { + if (entityTypes.includes(compType + 's') || entityTypes.includes(compType === 'manufacturer' ? 'manufacturers' : compType === 'property_owner' ? 'property_owners' : compType + 's')) { const count = Math.floor(plan.companies / 4); for (let i = 0; i < count; i++) { - const companyData = { - name: `Test ${compType} ${i + 1}`, + const level = getPopulationLevel(fieldDensity, i); + const companyData: any = { + name: `Test ${compType.replace('_', ' ')} ${i + 1}`, slug: `test-${compType}-${i + 1}`, - description: `Test ${compType} description`, - company_type: compType, - person_type: 'company', - founded_year: 2000 + company_type: compType }; + if (level >= 1) { + companyData.description = `Leading ${compType.replace('_', ' ')} in the amusement industry.`; + companyData.person_type = compType === 'designer' && Math.random() > 0.5 ? 'individual' : 'company'; + } + + if (level >= 2) { + companyData.founded_year = randomInt(1950, 2020); + const location = randomItem(CITIES); + companyData.headquarters_location = `${location.city}, ${location.country}`; + } + + if (level >= 3) { + companyData.website_url = `https://test-${compType}-${i + 1}.example.com`; + companyData.logo_url = `https://imagedelivery.net/test/${compType}-${i + 1}/logo`; + } + + if (level >= 4) { + companyData.card_image_id = `test-${compType}-card-${i + 1}`; + companyData.card_image_url = `https://imagedelivery.net/test/${compType}-${i + 1}/card`; + companyData.banner_image_id = `test-${compType}-banner-${i + 1}`; + companyData.banner_image_url = `https://imagedelivery.net/test/${compType}-${i + 1}/banner`; + } + await createSubmission(user.id, compType, companyData); createdCompanies[compType].push(`test-${compType}-${i + 1}`); summary.companies++; @@ -252,41 +317,112 @@ Deno.serve(async (req) => { } } - // Create rides (with dependencies if enabled) + // Create rides if (entityTypes.includes('rides') && includeDependencies && createdParks.length > 0) { for (let i = 0; i < plan.rides; i++) { - // Determine if this should be a conflict or version chain + const level = getPopulationLevel(fieldDensity, i); const shouldConflict = includeConflicts && createdRideSlugs.length > 0 && Math.random() < 0.15; const shouldVersionChain = includeVersionChains && createdRideSlugs.length > 0 && Math.random() < 0.15; let slug = `test-ride-${i + 1}`; if (shouldConflict) { - slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)]; + slug = randomItem(createdRideSlugs); summary.conflicts++; } else if (shouldVersionChain) { - slug = createdRideSlugs[Math.floor(Math.random() * createdRideSlugs.length)]; + slug = randomItem(createdRideSlugs); summary.versionChains++; } - // Get random park ID from database - const parkSlug = createdParks[Math.floor(Math.random() * createdParks.length)]; - const { data: parkData } = await supabase - .from('parks') - .select('id') - .eq('slug', parkSlug) - .maybeSingle(); + const parkSlug = randomItem(createdParks); + const { data: parkData } = await supabase.from('parks').select('id').eq('slug', parkSlug).maybeSingle(); - const rideData = { + const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride', 'dark_ride']); + const rideData: any = { name: shouldVersionChain ? `Test Ride ${slug} (Updated)` : `Test Ride ${i + 1}`, slug: slug, - description: shouldVersionChain ? 'Updated test ride description' : 'Test ride description', - category: ['roller_coaster', 'flat_ride', 'water_ride'][Math.floor(Math.random() * 3)], + category: category, status: 'operating', - park_id: parkData?.id || null, - opening_date: '2010-01-01' + park_id: parkData?.id || null }; - await createSubmission(user.id, 'ride', rideData); + if (level >= 1) { + rideData.opening_date = randomDate(2000, 2024); + rideData.description = `An exciting ${category.replace('_', ' ')} attraction.`; + rideData.height_requirement = randomInt(100, 140); + } + + if (level >= 2) { + rideData.max_speed_kmh = randomInt(40, 120); + rideData.max_height_meters = randomInt(20, 100); + rideData.duration_seconds = randomInt(60, 240); + rideData.capacity_per_hour = randomInt(500, 2000); + rideData.intensity_level = randomItem(['family', 'moderate', 'high', 'extreme']); + } + + if (level >= 3) { + if (createdCompanies.manufacturer.length > 0) { + const { data: mfgData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.manufacturer)).maybeSingle(); + if (mfgData) rideData.manufacturer_id = mfgData.id; + } + if (category === 'roller_coaster') { + rideData.inversions = randomInt(0, 7); + rideData.coaster_type = randomItem(['steel', 'wooden', 'hybrid']); + rideData.seating_type = randomItem(['sit_down', 'inverted', 'flying', 'stand_up']); + } + rideData.card_image_id = `test-ride-card-${i + 1}`; + rideData.card_image_url = `https://imagedelivery.net/test/ride-${i + 1}/card`; + } + + if (level >= 4) { + if (createdCompanies.designer.length > 0) { + const { data: designerData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.designer)).maybeSingle(); + if (designerData) rideData.designer_id = designerData.id; + } + rideData.length_meters = randomInt(500, 2000); + rideData.drop_height_meters = randomInt(10, 80); + rideData.max_g_force = (Math.random() * 4 + 2).toFixed(1); + rideData.banner_image_id = `test-ride-banner-${i + 1}`; + rideData.banner_image_url = `https://imagedelivery.net/test/ride-${i + 1}/banner`; + } + + const { submissionId, typeId } = await createSubmission(user.id, 'ride', rideData); + + // Add technical specs and stats for level 4 + if (level >= 4 && typeId && category === 'roller_coaster') { + // Add coaster stats + for (let s = 0; s < randomInt(2, 3); s++) { + await supabase.from('ride_submission_coaster_statistics').insert({ + ride_submission_id: typeId, + stat_name: randomItem(['Airtime Duration', 'Zero-G Time', 'Track Gauge']), + stat_value: Math.random() * 100, + unit: randomItem(['seconds', 'mm']), + category: 'technical' + }); + } + + // Add technical specs + for (let t = 0; t < randomInt(3, 5); t++) { + await supabase.from('ride_submission_technical_specifications').insert({ + ride_submission_id: typeId, + spec_name: randomItem(['Lift System', 'Brake System', 'Train Count', 'Track Material']), + spec_value: randomItem(['Chain lift', 'Magnetic brakes', '3 trains', 'Steel tubular']), + spec_type: 'system', + display_order: t + }); + } + + // Add former name + if (Math.random() > 0.7) { + await supabase.from('ride_submission_name_history').insert({ + ride_submission_id: typeId, + former_name: `Original Name ${i + 1}`, + date_changed: randomDate(2010, 2020), + reason: 'Rebranding', + order_index: 0 + }); + } + } + if (!shouldConflict && !shouldVersionChain) { createdRideSlugs.push(slug); } @@ -297,43 +433,128 @@ Deno.serve(async (req) => { // Create ride models if (entityTypes.includes('ride_models') && includeDependencies && createdCompanies.manufacturer.length > 0) { for (let i = 0; i < plan.rideModels; i++) { - const mfgSlug = createdCompanies.manufacturer[Math.floor(Math.random() * createdCompanies.manufacturer.length)]; - const { data: mfgData } = await supabase - .from('companies') - .select('id') - .eq('slug', mfgSlug) - .maybeSingle(); + const level = getPopulationLevel(fieldDensity, i); + const { data: mfgData } = await supabase.from('companies').select('id').eq('slug', randomItem(createdCompanies.manufacturer)).maybeSingle(); - const modelData = { + const category = randomItem(['roller_coaster', 'flat_ride', 'water_ride']); + const modelData: any = { name: `Test Model ${i + 1}`, slug: `test-model-${i + 1}`, - manufacturer_id: mfgData?.id || null, - category: 'roller_coaster', - ride_type: 'steel', - description: 'Test ride model' + category: category, + manufacturer_id: mfgData?.id || null }; + if (level >= 1) { + modelData.description = `Popular ${category.replace('_', ' ')} model.`; + modelData.ride_type = randomItem(['spinning', 'launch', 'suspended', 'family']); + } + + if (level >= 2) { + modelData.card_image_id = `test-model-card-${i + 1}`; + modelData.card_image_url = `https://imagedelivery.net/test/model-${i + 1}/card`; + } + + if (level >= 3) { + modelData.banner_image_id = `test-model-banner-${i + 1}`; + modelData.banner_image_url = `https://imagedelivery.net/test/model-${i + 1}/banner`; + } + await createSubmission(user.id, 'ride_model', modelData); summary.rideModels++; } } - const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2); + // Create photo submissions + if (entityTypes.includes('photos') && plan.photos > 0) { + const { data: approvedParks } = await supabase.from('parks').select('id').limit(Math.min(20, plan.photos)); + const { data: approvedRides } = await supabase.from('rides').select('id, park_id').limit(Math.min(20, plan.photos)); + + for (let i = 0; i < plan.photos; i++) { + const photoCount = randomInt(1, Math.min(10, Math.ceil(plan.photos / 50) + 3)); + const submissionId = crypto.randomUUID(); + const photoSubmissionId = crypto.randomUUID(); + + let entityType = 'park'; + let entityId = null; + let parentId = null; + + if (Math.random() > 0.5 && approvedParks && approvedParks.length > 0) { + entityType = 'park'; + entityId = randomItem(approvedParks).id; + } else if (approvedRides && approvedRides.length > 0) { + entityType = 'ride'; + const ride = randomItem(approvedRides); + entityId = ride.id; + parentId = ride.park_id; + } + + if (!entityId) continue; + + // Create content_submission + await supabase.from('content_submissions').insert({ + id: submissionId, + user_id: user.id, + submission_type: 'photo', + status: 'pending', + content: { + action: 'create', + metadata: { + is_test_data: true, + generated_at: new Date().toISOString(), + generator_version: '2.0.0', + preset + } + }, + submitted_at: new Date().toISOString() + }); + + // Create photo_submission + await supabase.from('photo_submissions').insert({ + id: photoSubmissionId, + submission_id: submissionId, + entity_type: entityType, + entity_id: entityId, + parent_id: parentId, + title: Math.random() > 0.5 ? `${entityType} Photos Collection ${i + 1}` : null + }); + + // Create photo_submission_items + for (let p = 0; p < photoCount; p++) { + const imageId = `test-photo-${crypto.randomUUID()}`; + await supabase.from('photo_submission_items').insert({ + photo_submission_id: photoSubmissionId, + cloudflare_image_id: imageId, + cloudflare_image_url: `https://imagedelivery.net/test/${imageId}/public`, + caption: Math.random() > 0.3 ? `Test photo ${p + 1} - Great view of the ${entityType}` : null, + title: Math.random() > 0.7 ? `Photo ${p + 1}` : null, + filename: `photo-${p + 1}.jpg`, + order_index: p, + file_size: randomInt(500000, 5000000), + mime_type: 'image/jpeg', + date_taken: Math.random() > 0.5 ? randomDate(2015, 2024) : null + }); + summary.totalPhotoItems++; + } + + summary.photos++; + } + } + + const executionTime = Date.now() - startTime; return new Response( JSON.stringify({ success: true, summary, - time: elapsedTime + time: (executionTime / 1000).toFixed(2) }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } catch (error) { console.error('Seed error:', error); - const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; return new Response( - JSON.stringify({ error: errorMessage }), + JSON.stringify({ error: error.message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); }