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'; import { withEdgeRetry } from '../_shared/retryHelper.ts'; interface NotificationPayload { submission_id: string; submission_type: string; submitter_name: string; action: string; content_preview: string; submitted_at: string; has_photos: boolean; item_count: number; is_escalated: boolean; } const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext) => { const payload: NotificationPayload = await req.json(); const { submission_id, submission_type, submitter_name, action, content_preview, submitted_at, has_photos, item_count, is_escalated } = payload; addSpanEvent(span, 'processing_moderator_notification', { submission_id, submission_type }); // Calculate relative time and priority const submittedDate = new Date(submitted_at); const now = new Date(); const waitTimeMs = now.getTime() - submittedDate.getTime(); const waitTimeHours = waitTimeMs / (1000 * 60 * 60); // Format relative time const relativeTime = (() => { const minutes = Math.floor(waitTimeMs / (1000 * 60)); const hours = Math.floor(waitTimeMs / (1000 * 60 * 60)); const days = Math.floor(waitTimeMs / (1000 * 60 * 60 * 24)); if (minutes < 1) return 'just now'; if (minutes < 60) return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`; if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''} ago`; return `${days} day${days !== 1 ? 's' : ''} ago`; })(); // Determine priority based on wait time const priority = waitTimeHours >= 24 ? 'urgent' : 'normal'; // Get the moderation-alert workflow const { data: workflow, error: workflowError } = await supabase .from('notification_templates') .select('workflow_id') .eq('category', 'moderation') .eq('is_active', true) .single(); if (workflowError || !workflow) { addSpanEvent(span, 'workflow_fetch_failed', { error: workflowError?.message }); throw new Error('Workflow not found or not active'); } // Generate idempotency key for duplicate prevention const { data: keyData, error: keyError } = await supabase .rpc('generate_notification_idempotency_key', { p_notification_type: 'moderation_submission', p_entity_id: submission_id, p_recipient_id: '00000000-0000-0000-0000-000000000000', p_event_data: { submission_type, action } }); const idempotencyKey = keyData || `mod_sub_${submission_id}_${Date.now()}`; // Check for duplicate within 24h window const { data: existingLog, error: logCheckError } = await supabase .from('notification_logs') .select('id') .eq('idempotency_key', idempotencyKey) .gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()) .maybeSingle(); if (existingLog) { // Duplicate detected await supabase.from('notification_logs').update({ is_duplicate: true }).eq('id', existingLog.id); addSpanEvent(span, 'duplicate_notification_prevented', { idempotencyKey, submission_id }); return { success: true, message: 'Duplicate notification prevented', idempotencyKey, }; } // Prepare enhanced notification payload const notificationPayload = { baseUrl: 'https://www.thrillwiki.com', itemType: submission_type, submitterName: submitter_name, submissionId: submission_id, action: action || 'create', moderationUrl: 'https://www.thrillwiki.com/admin/moderation', contentPreview: content_preview, submittedAt: submitted_at, relativeTime: relativeTime, priority: priority, hasPhotos: has_photos, itemCount: item_count, isEscalated: is_escalated, }; addSpanEvent(span, 'triggering_notification', { workflowId: workflow.workflow_id, priority }); // Send ONE notification to the moderation-submissions topic with retry const data = await withEdgeRetry( async () => { const { data: result, error } = await supabase.functions.invoke('trigger-notification', { body: { workflowId: workflow.workflow_id, topicKey: 'moderation-submissions', payload: notificationPayload, }, }); if (error) { const enhancedError = new Error(error.message || 'Notification trigger failed'); (enhancedError as any).status = error.status; throw enhancedError; } return result; }, { maxAttempts: 3, baseDelay: 1000 }, requestId, 'trigger-submission-notification' ); // Log notification with idempotency key const { error: logError } = await supabase.from('notification_logs').insert({ user_id: '00000000-0000-0000-0000-000000000000', notification_type: 'moderation_submission', idempotency_key: idempotencyKey, is_duplicate: false, metadata: { submission_id, submission_type, transaction_id: data?.transactionId } }); if (logError) { addSpanEvent(span, 'log_insertion_failed', { error: logError.message }); } addSpanEvent(span, 'notification_sent', { transactionId: data?.transactionId, topicKey: 'moderation-submissions' }); return { success: true, message: 'Moderator notifications sent via topic', topicKey: 'moderation-submissions', transactionId: data?.transactionId, }; }; serve(createEdgeFunction({ name: 'notify-moderators-submission', requireAuth: false, useServiceRole: true, corsHeaders, enableTracing: true, logRequests: true, }, handler));