diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 817f8611..26ed2f96 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -3413,6 +3413,47 @@ export type Database = { }, ] } + ride_model_submission_technical_specifications: { + Row: { + category: string | null + created_at: string | null + display_order: number | null + id: string + ride_model_submission_id: string + spec_name: string + spec_unit: string | null + spec_value: string + } + Insert: { + category?: string | null + created_at?: string | null + display_order?: number | null + id?: string + ride_model_submission_id: string + spec_name: string + spec_unit?: string | null + spec_value: string + } + Update: { + category?: string | null + created_at?: string | null + display_order?: number | null + id?: string + ride_model_submission_id?: string + spec_name?: string + spec_unit?: string | null + spec_value?: string + } + Relationships: [ + { + foreignKeyName: "fk_ride_model_submission" + columns: ["ride_model_submission_id"] + isOneToOne: false + referencedRelation: "ride_model_submissions" + referencedColumns: ["id"] + }, + ] + } ride_model_submissions: { Row: { banner_image_id: string | null diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index abda6458..abf06fe2 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -170,6 +170,15 @@ export interface CompanyFormData { card_image_id?: string; } +interface TechnicalSpecification { + spec_name: string; + spec_value: string; + spec_type?: 'string' | 'number' | 'boolean' | 'date'; + category?: string; + unit?: string; + display_order?: number; +} + export interface RideModelFormData { name: string; slug: string; @@ -182,6 +191,7 @@ export interface RideModelFormData { banner_image_id?: string; card_image_url?: string; card_image_id?: string; + _technical_specifications?: TechnicalSpecification[]; } /** @@ -1590,6 +1600,53 @@ export async function submitRideModelCreation( 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 }; } @@ -1664,6 +1721,53 @@ export async function submitRideModelUpdate( 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 }; } diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index ceff9dba..f03aa84a 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -1954,7 +1954,30 @@ async function createRideModel(supabase: any, data: any): Promise { let rideModelId: string; // Extract relational data before transformation - const technicalSpecifications = data._technical_specifications || []; + let technicalSpecifications = data._technical_specifications || []; + + // If no inline specs provided, fetch from submission table + if (technicalSpecifications.length === 0 && data.submission_id) { + const { data: submissionData } = await supabase + .from('ride_model_submissions') + .select('id') + .eq('submission_id', data.submission_id) + .single(); + + if (submissionData) { + const { data: submissionSpecs } = await supabase + .from('ride_model_submission_technical_specifications') + .select('*') + .eq('ride_model_submission_id', submissionData.id); + + if (submissionSpecs && submissionSpecs.length > 0) { + edgeLogger.info('Fetched technical specs from submission table', { + count: submissionSpecs.length + }); + technicalSpecifications = submissionSpecs; + } + } + } // Remove internal fields delete data._technical_specifications; diff --git a/supabase/migrations/20251106150117_a68ffa22-8213-468c-ab63-bad580073a8b.sql b/supabase/migrations/20251106150117_a68ffa22-8213-468c-ab63-bad580073a8b.sql new file mode 100644 index 00000000..581e063e --- /dev/null +++ b/supabase/migrations/20251106150117_a68ffa22-8213-468c-ab63-bad580073a8b.sql @@ -0,0 +1,84 @@ +-- Create submission table for ride model technical specifications +-- This ensures technical specs flow through the submission pipeline without data loss + +CREATE TABLE ride_model_submission_technical_specifications ( + id UUID DEFAULT gen_random_uuid() PRIMARY KEY, + ride_model_submission_id UUID NOT NULL, + spec_name TEXT NOT NULL, + spec_value TEXT NOT NULL, + spec_unit TEXT, + category TEXT, + display_order INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT now(), + + CONSTRAINT fk_ride_model_submission + FOREIGN KEY (ride_model_submission_id) + REFERENCES ride_model_submissions(id) + ON DELETE CASCADE, + + CONSTRAINT unique_ride_model_submission_spec + UNIQUE(ride_model_submission_id, spec_name) +); + +CREATE INDEX idx_ride_model_submission_specs_submission + ON ride_model_submission_technical_specifications(ride_model_submission_id); + +-- Enable RLS +ALTER TABLE ride_model_submission_technical_specifications ENABLE ROW LEVEL SECURITY; + +-- Moderators can view all submission specs +CREATE POLICY "Moderators can view all ride model submission specs" + ON ride_model_submission_technical_specifications + FOR SELECT + USING ( + is_moderator(auth.uid()) AND + ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +-- Users can view their own submission specs +CREATE POLICY "Users can view their own ride model submission specs" + ON ride_model_submission_technical_specifications + FOR SELECT + USING ( + EXISTS ( + SELECT 1 FROM ride_model_submissions rms + JOIN content_submissions cs ON cs.id = rms.submission_id + WHERE rms.id = ride_model_submission_technical_specifications.ride_model_submission_id + AND cs.user_id = auth.uid() + ) + ); + +-- Users can insert their own submission specs +CREATE POLICY "Users can insert their own ride model submission specs" + ON ride_model_submission_technical_specifications + FOR INSERT + WITH CHECK ( + EXISTS ( + SELECT 1 FROM ride_model_submissions rms + JOIN content_submissions cs ON cs.id = rms.submission_id + WHERE rms.id = ride_model_submission_technical_specifications.ride_model_submission_id + AND cs.user_id = auth.uid() + ) + AND NOT is_user_banned(auth.uid()) + ); + +-- Moderators can update submission specs +CREATE POLICY "Moderators can update ride model submission specs" + ON ride_model_submission_technical_specifications + FOR UPDATE + USING ( + is_moderator(auth.uid()) AND + ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +-- Moderators can delete submission specs +CREATE POLICY "Moderators can delete ride model submission specs" + ON ride_model_submission_technical_specifications + FOR DELETE + USING ( + is_moderator(auth.uid()) AND + ((NOT has_mfa_enabled(auth.uid())) OR has_aal2()) + ); + +COMMENT ON TABLE ride_model_submission_technical_specifications IS + 'Stores technical specifications for ride models during moderation - prevents data loss in submission pipeline'; \ No newline at end of file