Files
thrilltrack-explorer/supabase/functions/notify-user-submission-status/index.ts
gpt-engineer-app[bot] 8ee548fd27 Migrate Phase 1 user-facing functions
Refactor export-user-data, notify-user-submission-status, and resend-deletion-code to use createEdgeFunction wrapper. Remove manual CORS, auth, rate limiting boilerplate; adopt standardized EdgeFunctionContext (supabase, user, span, requestId), and integrate built-in tracing, rate limiting, and logging through the wrapper. Update handlers to rely on wrapper context and ensure consistent error handling and observability.
2025-11-11 20:35:45 +00:00

232 lines
6.4 KiB
TypeScript

import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
import { corsHeaders } from '../_shared/cors.ts';
import { addSpanEvent } from '../_shared/logger.ts';
interface RequestBody {
submission_id: string;
user_id: string;
submission_type: string;
status: 'approved' | 'rejected';
reviewer_notes?: string;
}
async function constructEntityURL(
supabase: any,
submissionType: string,
itemData: any
): Promise<string> {
const baseURL = 'https://www.thrillwiki.com';
if (submissionType === 'park') {
const parkSlug = itemData.slug;
return `${baseURL}/parks/${parkSlug}`;
}
if (submissionType === 'ride') {
const rideSlug = itemData.slug;
const parkId = itemData.park_id;
if (!parkId) {
return `${baseURL}/rides/${rideSlug}`;
}
// Fetch park slug
const { data: park } = await supabase
.from('parks')
.select('slug')
.eq('id', parkId)
.maybeSingle();
const parkSlug = park?.slug || 'unknown';
return `${baseURL}/parks/${parkSlug}/rides/${rideSlug}`;
}
if (submissionType === 'company') {
const companySlug = itemData.slug;
const companyType = itemData.company_type;
if (companyType === 'manufacturer') {
return `${baseURL}/manufacturers/${companySlug}`;
} else if (companyType === 'operator') {
return `${baseURL}/operators/${companySlug}`;
} else if (companyType === 'property_owner') {
return `${baseURL}/owners/${companySlug}`;
} else if (companyType === 'designer') {
return `${baseURL}/designers/${companySlug}`;
}
return `${baseURL}/companies/${companySlug}`;
}
if (submissionType === 'ride_model') {
const modelSlug = itemData.slug;
const manufacturerId = itemData.manufacturer_id;
if (!manufacturerId) {
return `${baseURL}/models/${modelSlug}`;
}
// Fetch manufacturer slug
const { data: manufacturer } = await supabase
.from('companies')
.select('slug')
.eq('id', manufacturerId)
.eq('company_type', 'manufacturer')
.maybeSingle();
const manufacturerSlug = manufacturer?.slug || 'unknown';
return `${baseURL}/manufacturers/${manufacturerSlug}/models/${modelSlug}`;
}
return `${baseURL}`;
}
const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext) => {
const { submission_id, user_id, submission_type, status, reviewer_notes } = await req.json() as RequestBody;
addSpanEvent(span, 'notification_request', {
submissionId: submission_id,
userId: user_id,
status
});
// Fetch submission items to get entity data
const { data: items, error: itemsError } = await supabase
.from('submission_items')
.select('item_data')
.eq('submission_id', submission_id)
.order('order_index', { ascending: true })
.limit(1)
.maybeSingle();
if (itemsError) {
throw new Error(`Failed to fetch submission items: ${itemsError.message}`);
}
if (!items || !items.item_data) {
throw new Error('No submission items found');
}
// Extract entity data
const entityName = items.item_data.name || 'your submission';
const entityType = submission_type.replace('_', ' ');
// Construct entity URL
const entityURL = await constructEntityURL(supabase, submission_type, items.item_data);
// Determine workflow and build payload based on status
const workflowId = status === 'approved' ? 'submission-approved' : 'submission-rejected';
let payload: Record<string, string>;
if (status === 'approved') {
payload = {
baseUrl: 'https://www.thrillwiki.com',
entityType,
entityName,
submissionId: submission_id,
entityURL,
moderationNotes: reviewer_notes || '',
};
} else {
payload = {
baseUrl: 'https://www.thrillwiki.com',
rejectionReason: reviewer_notes || 'No reason provided',
entityType,
entityName,
entityURL,
actualStatus: 'rejected',
};
}
// Generate idempotency key for duplicate prevention
const { data: keyData, error: keyError } = await supabase
.rpc('generate_notification_idempotency_key', {
p_notification_type: `submission_${status}`,
p_entity_id: submission_id,
p_recipient_id: user_id,
});
const idempotencyKey = keyData || `user_sub_${submission_id}_${user_id}_${status}_${Date.now()}`;
// Check for duplicate within 24h window
const { data: existingLog, error: logCheckError } = await supabase
.from('notification_logs')
.select('id')
.eq('user_id', user_id)
.eq('idempotency_key', idempotencyKey)
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
.maybeSingle();
if (existingLog) {
// Duplicate detected - log and skip
await supabase.from('notification_logs').update({
is_duplicate: true
}).eq('id', existingLog.id);
addSpanEvent(span, 'duplicate_notification_prevented', {
idempotencyKey,
submissionId: submission_id
});
return {
success: true,
message: 'Duplicate notification prevented',
idempotencyKey,
};
}
addSpanEvent(span, 'sending_notification', {
workflowId,
entityName,
idempotencyKey
});
// Call trigger-notification function
const { data: notificationResult, error: notificationError } = await supabase.functions.invoke(
'trigger-notification',
{
body: {
workflowId,
subscriberId: user_id,
payload,
},
}
);
if (notificationError) {
throw new Error(`Failed to trigger notification: ${notificationError.message}`);
}
// Log notification in notification_logs with idempotency key
await supabase.from('notification_logs').insert({
user_id,
notification_type: `submission_${status}`,
idempotency_key: idempotencyKey,
is_duplicate: false,
metadata: {
submission_id,
submission_type,
transaction_id: notificationResult?.transactionId
}
});
addSpanEvent(span, 'notification_sent', {
transactionId: notificationResult?.transactionId
});
return {
success: true,
transactionId: notificationResult?.transactionId,
};
};
serve(createEdgeFunction({
name: 'notify-user-submission-status',
requireAuth: false,
useServiceRole: true,
corsHeaders,
enableTracing: true,
logRequests: true,
}, handler));