mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 12:11:17 -05:00
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.
232 lines
6.4 KiB
TypeScript
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));
|