import { supabase } from '@/lib/supabaseClient'; import type { Json } from '@/integrations/supabase/types'; import { uploadPendingImages } from './imageUploadHelper'; import { CompanyFormData, TempCompanyData } from '@/types/company'; import { handleError } from './errorHandler'; import { withRetry, isRetryableError } from './retryHelpers'; import { logger } from './logger'; import { checkSubmissionRateLimit, recordSubmissionAttempt } from './submissionRateLimiter'; import { sanitizeErrorMessage } from './errorSanitizer'; import { reportRateLimitViolation, reportBanEvasionAttempt } from './pipelineAlerts'; export type { CompanyFormData, TempCompanyData }; /** * Rate limiting helper - checks rate limits before allowing submission */ function checkRateLimitOrThrow(userId: string, action: string): void { const rateLimit = checkSubmissionRateLimit(userId); if (!rateLimit.allowed) { const sanitizedMessage = sanitizeErrorMessage(rateLimit.reason || 'Rate limit exceeded'); logger.warn('[RateLimit] Company submission blocked', { userId, action, reason: rateLimit.reason, retryAfter: rateLimit.retryAfter, }); // Report to system alerts for admin visibility reportRateLimitViolation(userId, action, rateLimit.retryAfter || 60).catch(() => { // Non-blocking - don't fail submission if alert fails }); throw new Error(sanitizedMessage); } logger.info('[RateLimit] Company submission allowed', { userId, action, remaining: rateLimit.remaining, }); } export async function submitCompanyCreation( data: CompanyFormData, companyType: 'manufacturer' | 'designer' | 'operator' | 'property_owner', userId: string ) { // Phase 3: Rate limiting check checkRateLimitOrThrow(userId, 'company_creation'); recordSubmissionAttempt(userId); // Check if user is banned (with quick retry for read operation) 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) { // Report ban evasion attempt reportBanEvasionAttempt(userId, 'company_creation').catch(() => { // Non-blocking - don't fail if alert fails }); throw new Error('Account suspended. Contact support for assistance.'); } // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error: unknown) { handleError(error, { action: 'Upload company images', metadata: { companyType }, }); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create submission with retry logic const retryId = crypto.randomUUID(); 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: companyType, content: { action: 'create' }, status: 'pending' as const }) .select('id') .single(); if (submissionError) throw submissionError; // Create the submission item with actual company data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: companyType, item_data: { name: data.name, slug: data.slug, description: data.description, person_type: data.person_type, website_url: data.website_url, founded_year: data.founded_year, headquarters_location: data.headquarters_location, company_type: companyType, 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 company submission', { attempt, delay, companyType }); // Emit event for UI indicator window.dispatchEvent(new CustomEvent('submission-retry', { detail: { id: retryId, attempt, maxAttempts: 3, delay, type: companyType } })); }, shouldRetry: (error) => { // Don't retry validation/business logic errors 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; if (message.includes('permission')) return false; } return isRetryableError(error); } } ).then((data) => { // Emit success event window.dispatchEvent(new CustomEvent('submission-retry-success', { detail: { id: retryId } })); return data; }).catch((error) => { const errorId = handleError(error, { action: `${companyType} submission`, metadata: { retriesExhausted: true }, }); // Emit failure event window.dispatchEvent(new CustomEvent('submission-retry-failed', { detail: { id: retryId, errorId } })); throw error; }); return result; } export async function submitCompanyUpdate( companyId: string, data: CompanyFormData, userId: string ) { // Phase 3: Rate limiting check checkRateLimitOrThrow(userId, 'company_update'); recordSubmissionAttempt(userId); // Check if user is banned (with quick retry for read operation) 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) { // Report ban evasion attempt reportBanEvasionAttempt(userId, 'company_update').catch(() => { // Non-blocking - don't fail if alert fails }); throw new Error('Account suspended. Contact support for assistance.'); } // Fetch existing company data (all fields for original_data) const { data: existingCompany, error: fetchError } = await supabase .from('companies') .select('id, name, slug, description, company_type, person_type, logo_url, card_image_url, banner_image_url, banner_image_id, card_image_id, headquarters_location, website_url, founded_year, founded_date, founded_date_precision') .eq('id', companyId) .single(); if (fetchError) throw fetchError; if (!existingCompany) throw new Error('Company not found'); // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { try { const uploadedImages = await uploadPendingImages(data.images.uploaded); processedImages = { ...data.images, uploaded: uploadedImages }; } catch (error: unknown) { handleError(error, { action: 'Upload company images for update', metadata: { companyType: existingCompany.company_type, companyId }, }); throw new Error('Failed to upload images. Please check your connection and try again.'); } } // Create submission with retry logic const retryId = crypto.randomUUID(); 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: existingCompany.company_type, content: { action: 'edit', company_id: companyId }, status: 'pending' as const }) .select('id') .single(); if (submissionError) throw submissionError; // Create the submission item with actual company data AND original data const { error: itemError } = await supabase .from('submission_items') .insert({ submission_id: submissionData.id, item_type: existingCompany.company_type, item_data: { company_id: companyId, name: data.name, slug: data.slug, description: data.description, person_type: data.person_type, website_url: data.website_url, founded_year: data.founded_year, headquarters_location: data.headquarters_location, 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 company update', { attempt, delay, companyId }); // Emit event for UI indicator window.dispatchEvent(new CustomEvent('submission-retry', { detail: { id: retryId, attempt, maxAttempts: 3, delay, type: `${existingCompany.company_type} update` } })); }, shouldRetry: (error) => { // Don't retry validation/business logic errors 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; if (message.includes('permission')) return false; } return isRetryableError(error); } } ).then((data) => { // Emit success event window.dispatchEvent(new CustomEvent('submission-retry-success', { detail: { id: retryId } })); return data; }).catch((error) => { const errorId = handleError(error, { action: `${existingCompany.company_type} update`, metadata: { retriesExhausted: true, companyId }, }); // Emit failure event window.dispatchEvent(new CustomEvent('submission-retry-failed', { detail: { id: retryId, errorId } })); throw error; }); return result; }