diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 6b5764f6..93a9e2c2 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -2000,9 +2000,36 @@ export async function submitManufacturerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'manufacturer_creation'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start manufacturer submission', 'submitManufacturerCreation', { userId }); + // Validate required fields client-side assertValid(validateCompanyCreateFields({ ...data, company_type: 'manufacturer' })); + // 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.'); + } + + // Upload images let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -2016,40 +2043,66 @@ export async function submitManufacturerCreation( } } - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'manufacturer', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'manufacturer', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'manufacturer', - action_type: 'create', - item_data: { - // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data - ...(() => { - const { images, ...dataWithoutImages } = data; - return dataWithoutImages; - })(), - company_type: 'manufacturer', - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'manufacturer', + action_type: 'create', + item_data: { + // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data + ...(() => { + const { images, ...dataWithoutImages } = data; + return dataWithoutImages; + })(), + company_type: 'manufacturer', + images: processedImages as unknown as Json + }, + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying manufacturer submission', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'manufacturer' } + })); }, - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitManufacturerUpdate( @@ -2057,6 +2110,32 @@ export async function submitManufacturerUpdate( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'manufacturer_update'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start manufacturer update', 'submitManufacturerUpdate', { userId, companyId }); + + // 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.'); + } + const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') @@ -2073,47 +2152,100 @@ export async function submitManufacturerUpdate( let processedImages = data.images; - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'manufacturer', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'manufacturer', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'manufacturer', - action_type: 'edit', - item_data: { - ...extractChangedFields(data, existingCompany as any), - company_id: companyId, // Always include for relational integrity - company_type: 'manufacturer', // Always include for entity type discrimination - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'manufacturer', + action_type: 'edit', + item_data: { + ...extractChangedFields(data, existingCompany as any), + company_id: companyId, // Always include for relational integrity + company_type: 'manufacturer', // Always include for entity type discrimination + images: processedImages as unknown as Json + }, + original_data: JSON.parse(JSON.stringify(existingCompany)), + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying manufacturer update', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'manufacturer_update' } + })); }, - original_data: JSON.parse(JSON.stringify(existingCompany)), - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitDesignerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'designer_creation'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start designer submission', 'submitDesignerCreation', { userId }); + // Validate required fields client-side assertValid(validateCompanyCreateFields({ ...data, company_type: 'designer' })); + // 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.'); + } + + // Upload images let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -2127,40 +2259,66 @@ export async function submitDesignerCreation( } } - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'designer', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'designer', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'designer', - action_type: 'create', - item_data: { - // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data - ...(() => { - const { images, ...dataWithoutImages } = data; - return dataWithoutImages; - })(), - company_type: 'designer', - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'designer', + action_type: 'create', + item_data: { + // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data + ...(() => { + const { images, ...dataWithoutImages } = data; + return dataWithoutImages; + })(), + company_type: 'designer', + images: processedImages as unknown as Json + }, + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying designer submission', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'designer' } + })); }, - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitDesignerUpdate( @@ -2168,6 +2326,32 @@ export async function submitDesignerUpdate( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'designer_update'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start designer update', 'submitDesignerUpdate', { userId, companyId }); + + // 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.'); + } + const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') @@ -2184,47 +2368,100 @@ export async function submitDesignerUpdate( let processedImages = data.images; - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'designer', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'designer', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'designer', - action_type: 'edit', - item_data: { - ...extractChangedFields(data, existingCompany as any), - company_id: companyId, // Always include for relational integrity - company_type: 'designer', // Always include for entity type discrimination - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'designer', + action_type: 'edit', + item_data: { + ...extractChangedFields(data, existingCompany as any), + company_id: companyId, // Always include for relational integrity + company_type: 'designer', // Always include for entity type discrimination + images: processedImages as unknown as Json + }, + original_data: JSON.parse(JSON.stringify(existingCompany)), + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying designer update', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'designer_update' } + })); }, - original_data: JSON.parse(JSON.stringify(existingCompany)), - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitOperatorCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'operator_creation'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start operator submission', 'submitOperatorCreation', { userId }); + // Validate required fields client-side assertValid(validateCompanyCreateFields({ ...data, company_type: 'operator' })); + // 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.'); + } + + // Upload images let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -2238,40 +2475,66 @@ export async function submitOperatorCreation( } } - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'operator', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'operator', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'operator', - action_type: 'create', - item_data: { - // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data - ...(() => { - const { images, ...dataWithoutImages } = data; - return dataWithoutImages; - })(), - company_type: 'operator', - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'operator', + action_type: 'create', + item_data: { + // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data + ...(() => { + const { images, ...dataWithoutImages } = data; + return dataWithoutImages; + })(), + company_type: 'operator', + images: processedImages as unknown as Json + }, + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying operator submission', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'operator' } + })); }, - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitOperatorUpdate( @@ -2279,6 +2542,32 @@ export async function submitOperatorUpdate( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'operator_update'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start operator update', 'submitOperatorUpdate', { userId, companyId }); + + // 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.'); + } + const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') @@ -2295,47 +2584,100 @@ export async function submitOperatorUpdate( let processedImages = data.images; - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'operator', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'operator', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'operator', - action_type: 'edit', - item_data: { - ...extractChangedFields(data, existingCompany as any), - company_id: companyId, // Always include for relational integrity - company_type: 'operator', // Always include for entity type discrimination - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'operator', + action_type: 'edit', + item_data: { + ...extractChangedFields(data, existingCompany as any), + company_id: companyId, // Always include for relational integrity + company_type: 'operator', // Always include for entity type discrimination + images: processedImages as unknown as Json + }, + original_data: JSON.parse(JSON.stringify(existingCompany)), + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying operator update', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'operator_update' } + })); }, - original_data: JSON.parse(JSON.stringify(existingCompany)), - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitPropertyOwnerCreation( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'property_owner_creation'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start property owner submission', 'submitPropertyOwnerCreation', { userId }); + // Validate required fields client-side assertValid(validateCompanyCreateFields({ ...data, company_type: 'property_owner' })); + // 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.'); + } + + // Upload images let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { @@ -2349,40 +2691,66 @@ export async function submitPropertyOwnerCreation( } } - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'property_owner', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'property_owner', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'property_owner', - action_type: 'create', - item_data: { - // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data - ...(() => { - const { images, ...dataWithoutImages } = data; - return dataWithoutImages; - })(), - company_type: 'property_owner', - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'property_owner', + action_type: 'create', + item_data: { + // ✅ FIXED: Don't use extractChangedFields for CREATE - include ALL data + ...(() => { + const { images, ...dataWithoutImages } = data; + return dataWithoutImages; + })(), + company_type: 'property_owner', + images: processedImages as unknown as Json + }, + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying property owner submission', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'property_owner' } + })); }, - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } export async function submitPropertyOwnerUpdate( @@ -2390,6 +2758,32 @@ export async function submitPropertyOwnerUpdate( data: CompanyFormData, userId: string ): Promise<{ submitted: boolean; submissionId: string }> { + // Rate limiting check + checkRateLimitOrThrow(userId, 'property_owner_update'); + recordSubmissionAttempt(userId); + + // Breadcrumb tracking + breadcrumb.userAction('Start property owner update', 'submitPropertyOwnerUpdate', { userId, companyId }); + + // 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.'); + } + const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('*') @@ -2406,38 +2800,64 @@ export async function submitPropertyOwnerUpdate( let processedImages = data.images; - const { data: submissionData, error: submissionError } = await supabase - .from('content_submissions') - .insert({ - user_id: userId, - submission_type: 'property_owner', - status: 'pending' as const - }) - .select() - .single(); + // Submit with retry logic + breadcrumb.apiCall('content_submissions', 'INSERT'); + const result = await withRetry( + async () => { + const { data: submissionData, error: submissionError } = await supabase + .from('content_submissions') + .insert({ + user_id: userId, + submission_type: 'property_owner', + status: 'pending' as const + }) + .select() + .single(); - if (submissionError) throw submissionError; + if (submissionError) throw submissionError; - const { error: itemError } = await supabase - .from('submission_items') - .insert({ - submission_id: submissionData.id, - item_type: 'property_owner', - action_type: 'edit', - item_data: { - ...extractChangedFields(data, existingCompany as any), - company_id: companyId, // Always include for relational integrity - company_type: 'property_owner', // Always include for entity type discrimination - images: processedImages as unknown as Json + const { error: itemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'property_owner', + action_type: 'edit', + item_data: { + ...extractChangedFields(data, existingCompany as any), + company_id: companyId, // Always include for relational integrity + company_type: 'property_owner', // Always include for entity type discrimination + images: processedImages as unknown as Json + }, + original_data: JSON.parse(JSON.stringify(existingCompany)), + status: 'pending' as const, + order_index: 0 + }); + + if (itemError) throw itemError; + + return { submitted: true, submissionId: submissionData.id }; + }, + { + maxAttempts: 3, + onRetry: (attempt, error, delay) => { + logger.warn('Retrying property owner update', { attempt, delay }); + window.dispatchEvent(new CustomEvent('submission-retry', { + detail: { attempt, maxAttempts: 3, delay, type: 'property_owner_update' } + })); }, - original_data: JSON.parse(JSON.stringify(existingCompany)), - status: 'pending' as const, - order_index: 0 - }); + 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); + } + } + ); - if (itemError) throw itemError; - - return { submitted: true, submissionId: submissionData.id }; + return result; } /**