diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 93a9e2c2..f5bc4823 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -1755,15 +1755,30 @@ export async function submitRideModelCreation( data: RideModelFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'ride_model_creation'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start ride model submission', 'submitRideModelCreation', { userId }); + // Validate required fields client-side assertValid(validateRideModelCreateFields(data)); - // Check if user is banned - const { data: profile } = await supabase - .from('profiles') - .select('banned') - .eq('user_id', userId) - .single(); + // Ban check with retry logic + const { withRetry } = await import('./retryHelpers'); + breadcrumb.apiCall('profiles', 'SELECT'); + const profile = await withRetry( + async () => { + const { data: profile } = await supabase + .from('profiles') + .select('banned') + .eq('user_id', userId) + .single(); + return profile; + }, + { maxAttempts: 2 } + ); if (profile?.banned) { throw new Error('Account suspended. Contact support for assistance.'); @@ -1786,88 +1801,114 @@ export async function submitRideModelCreation( } } - // Create the main submission record - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'ride_model', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + // Create the main submission record + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'ride_model', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - // Create the submission item with actual ride model data - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'ride_model', - action_type: 'create', - item_data: { - // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data - ...(() => { - const { images, ...dataWithoutImages } = data; - return dataWithoutImages; - })(), - images: processedImages as unknown as Json + // Create the submission item with actual ride model data + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'ride_model', + action_type: 'create', + item_data: { + // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data + ...(() => { + const { images, ...dataWithoutImages } = data; + return dataWithoutImages; + })(), + images: processedImages as unknown as Json + }, + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + // Insert into ride_model_submissions table for relational integrity + const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase + .from('ride_model_submissions') + .insert({ + submission_id: submissionData.id, + name: data.name, + slug: data.slug, + manufacturer_id: data.manufacturer_id, + category: data.category, + ride_type: data.ride_type || data.category, + description: data.description || null, + banner_image_url: data.banner_image_url || null, + banner_image_id: data.banner_image_id || null, + card_image_url: data.card_image_url || null, + card_image_id: data.card_image_id || null + }) + .select() + .single(); + + if (rideModelSubmissionError) { + logger.error('Failed to insert ride model submission', { error: rideModelSubmissionError }); + throw rideModelSubmissionError; + } + + // Insert technical specifications into submission table + if ((data as any)._technical_specifications?.length > 0) { + const { error: techSpecError } = await supabase + .from('ride_model_submission_technical_specifications') + .insert( + (data as any)._technical_specifications.map((spec: any) => ({ + ride_model_submission_id: rideModelSubmissionData.id, + spec_name: spec.spec_name, + spec_value: spec.spec_value, + spec_unit: spec.spec_unit || null, + category: spec.category || null, + display_order: spec.display_order || 0 + })) + ); + + if (techSpecError) { + logger.error('Failed to insert ride model technical specs', { error: techSpecError }); + throw techSpecError; + } + + logger.log('✅ Ride model technical specifications inserted:', (data as any)._technical_specifications.length); + } + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying ride model submission', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'ride_model' } + })); }, - status: 'pending' as const, - order_index: 0 - }); - - if (itemError) throw itemError; - - // Insert into ride_model_submissions table for relational integrity - const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase - .from('ride_model_submissions') - .insert({ - submission_id: submissionData.id, - name: data.name, - slug: data.slug, - manufacturer_id: data.manufacturer_id, - category: data.category, - ride_type: data.ride_type || data.category, - description: data.description || null, - banner_image_url: data.banner_image_url || null, - banner_image_id: data.banner_image_id || null, - card_image_url: data.card_image_url || null, - card_image_id: data.card_image_id || null - }) - .select() - .single(); - - if (rideModelSubmissionError) { - logger.error('Failed to insert ride model submission', { error: rideModelSubmissionError }); - throw rideModelSubmissionError; - } - - // Insert technical specifications into submission table - if ((data as any)._technical_specifications?.length > 0) { - const { error: techSpecError } = await supabase - .from('ride_model_submission_technical_specifications') - .insert( - (data as any)._technical_specifications.map((spec: any) => ({ - ride_model_submission_id: rideModelSubmissionData.id, - spec_name: spec.spec_name, - spec_value: spec.spec_value, - spec_unit: spec.spec_unit || null, - category: spec.category || null, - display_order: spec.display_order || 0 - })) - ); - - if (techSpecError) { - logger.error('Failed to insert ride model technical specs', { error: techSpecError }); - throw techSpecError; + shouldRetry: (error) => { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + if (message.includes('required')) return false; + if (message.includes('banned')) return false; + if (message.includes('slug')) return false; + } + return isRetryableError(error); + } } - - logger.log('✅ Ride model technical specifications inserted:', (data as any)._technical_specifications.length); - } + ); - return { submitted: true, submissionId: submissionData.id }; + return result; } /** @@ -1881,12 +1922,27 @@ export async function submitRideModelUpdate( data: RideModelFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { - // Check if user is banned - const { data: profile } = await supabase - .from('profiles') - .select('banned') - .eq('user_id', userId) - .single(); + // Rate limiting check + checkRateLimitOrThrow(userId, 'ride_model_update'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start ride model update', 'submitRideModelUpdate', { userId, rideModelId }); + + // Ban check with retry logic + const { withRetry } = await import('./retryHelpers'); + breadcrumb.apiCall('profiles', 'SELECT'); + const profile = await withRetry( + async () => { + const { data: profile } = await supabase + .from('profiles') + .select('banned') + .eq('user_id', userId) + .single(); + return profile; + }, + { maxAttempts: 2 } + ); if (profile?.banned) { throw new Error('Account suspended. Contact support for assistance.'); @@ -1909,86 +1965,112 @@ export async function submitRideModelUpdate( let processedImages = data.images; - // Create the main submission record - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'ride_model', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + // Create the main submission record + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'ride_model', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - // Create the submission item with actual ride model data - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'ride_model', - action_type: 'edit', - item_data: { - ...extractChangedFields(data, existingModel as any), - ride_model_id: rideModelId, // Always include for relational integrity - images: processedImages as unknown as Json + // Create the submission item with actual ride model data + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'ride_model', + action_type: 'edit', + item_data: { + ...extractChangedFields(data, existingModel as any), + ride_model_id: rideModelId, // Always include for relational integrity + images: processedImages as unknown as Json + }, + original_data: JSON.parse(JSON.stringify(existingModel)), + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + // Insert into ride_model_submissions table for relational integrity + const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase + .from('ride_model_submissions') + .insert({ + submission_id: submissionData.id, + name: data.name, + slug: data.slug, + manufacturer_id: data.manufacturer_id, + category: data.category, + ride_type: data.ride_type || data.category, + description: data.description || null, + banner_image_url: data.banner_image_url || null, + banner_image_id: data.banner_image_id || null, + card_image_url: data.card_image_url || null, + card_image_id: data.card_image_id || null + }) + .select() + .single(); + + if (rideModelSubmissionError) { + logger.error('Failed to insert ride model update submission', { error: rideModelSubmissionError }); + throw rideModelSubmissionError; + } + + // Insert technical specifications into submission table + if ((data as any)._technical_specifications?.length > 0) { + const { error: techSpecError } = await supabase + .from('ride_model_submission_technical_specifications') + .insert( + (data as any)._technical_specifications.map((spec: any) => ({ + ride_model_submission_id: rideModelSubmissionData.id, + spec_name: spec.spec_name, + spec_value: spec.spec_value, + spec_unit: spec.spec_unit || null, + category: spec.category || null, + display_order: spec.display_order || 0 + })) + ); + + if (techSpecError) { + logger.error('Failed to insert ride model update technical specs', { error: techSpecError }); + throw techSpecError; + } + + logger.log('✅ Ride model update technical specifications inserted:', (data as any)._technical_specifications.length); + } + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying ride model update', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'ride_model_update' } + })); }, - original_data: JSON.parse(JSON.stringify(existingModel)), - status: 'pending' as const, - order_index: 0 - }); - - if (itemError) throw itemError; - - // Insert into ride_model_submissions table for relational integrity - const { data: rideModelSubmissionData, error: rideModelSubmissionError } = await supabase - .from('ride_model_submissions') - .insert({ - submission_id: submissionData.id, - name: data.name, - slug: data.slug, - manufacturer_id: data.manufacturer_id, - category: data.category, - ride_type: data.ride_type || data.category, - description: data.description || null, - banner_image_url: data.banner_image_url || null, - banner_image_id: data.banner_image_id || null, - card_image_url: data.card_image_url || null, - card_image_id: data.card_image_id || null - }) - .select() - .single(); - - if (rideModelSubmissionError) { - logger.error('Failed to insert ride model update submission', { error: rideModelSubmissionError }); - throw rideModelSubmissionError; - } - - // Insert technical specifications into submission table - if ((data as any)._technical_specifications?.length > 0) { - const { error: techSpecError } = await supabase - .from('ride_model_submission_technical_specifications') - .insert( - (data as any)._technical_specifications.map((spec: any) => ({ - ride_model_submission_id: rideModelSubmissionData.id, - spec_name: spec.spec_name, - spec_value: spec.spec_value, - spec_unit: spec.spec_unit || null, - category: spec.category || null, - display_order: spec.display_order || 0 - })) - ); - - if (techSpecError) { - logger.error('Failed to insert ride model update technical specs', { error: techSpecError }); - throw techSpecError; + shouldRetry: (error) => { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + if (message.includes('required')) return false; + if (message.includes('banned')) return false; + if (message.includes('slug')) return false; + } + return isRetryableError(error); + } } - - logger.log('✅ Ride model update technical specifications inserted:', (data as any)._technical_specifications.length); - } + ); - return { submitted: true, submissionId: submissionData.id }; + return result; } /**