import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; import { withEdgeRetry } from '../_shared/retryHelper.ts'; const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; 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; } serve(async (req) => { const tracking = startRequest(); if (req.method === 'OPTIONS') { return new Response(null, { headers: { ...corsHeaders, 'X-Request-ID': tracking.requestId } }); } try { const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); 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; edgeLogger.info('Notifying moderators about submission via topic', { action: 'notify_moderators', requestId: tracking.requestId, submission_id, submission_type, content_preview }); // 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) { const duration = endRequest(tracking); edgeLogger.error('Error fetching workflow', { action: 'notify_moderators', requestId: tracking.requestId, duration, error: workflowError }); return new Response( JSON.stringify({ success: false, error: 'Workflow not found or not active', requestId: tracking.requestId }), { headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, status: 500, } ); } // 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', // Topic-based, use placeholder 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 - log and skip await supabase.from('notification_logs').update({ is_duplicate: true }).eq('id', existingLog.id); edgeLogger.info('Duplicate notification prevented', { action: 'notify_moderators', requestId: tracking.requestId, idempotencyKey, submission_id }); return new Response( JSON.stringify({ success: true, message: 'Duplicate notification prevented', idempotencyKey, requestId: tracking.requestId, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, status: 200, } ); } // Prepare enhanced notification payload const notificationPayload = { baseUrl: 'https://www.thrillwiki.com', // Basic info itemType: submission_type, submitterName: submitter_name, submissionId: submission_id, action: action || 'create', moderationUrl: 'https://www.thrillwiki.com/admin/moderation', // Enhanced content contentPreview: content_preview, // Timing information submittedAt: submitted_at, relativeTime: relativeTime, priority: priority, // Additional metadata hasPhotos: has_photos, itemCount: item_count, isEscalated: is_escalated, }; // Send ONE notification to the moderation-submissions topic with retry // All subscribers (moderators) will receive it automatically 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 }, tracking.requestId, 'trigger-submission-notification' ); // Log notification in notification_logs with idempotency key const { error: logError } = await supabase.from('notification_logs').insert({ user_id: '00000000-0000-0000-0000-000000000000', // Topic-based notification_type: 'moderation_submission', idempotency_key: idempotencyKey, is_duplicate: false, metadata: { submission_id, submission_type, transaction_id: data?.transactionId } }); if (logError) { // Non-blocking - notification was sent successfully, log failure shouldn't fail the request edgeLogger.warn('Failed to log notification in notification_logs', { action: 'notify_moderators', requestId: tracking.requestId, error: logError.message, submissionId: submission_id }); } const duration = endRequest(tracking); edgeLogger.info('Successfully notified all moderators via topic', { action: 'notify_moderators', requestId: tracking.requestId, traceId: tracking.traceId, duration, transactionId: data?.transactionId }); return new Response( JSON.stringify({ success: true, message: 'Moderator notifications sent via topic', topicKey: 'moderation-submissions', transactionId: data?.transactionId, requestId: tracking.requestId, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, status: 200, } ); } catch (error: any) { const duration = endRequest(tracking); edgeLogger.error('Error in notify-moderators-submission', { action: 'notify_moderators', requestId: tracking.requestId, duration, error: error.message }); return new Response( JSON.stringify({ success: false, error: error.message, requestId: tracking.requestId, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, status: 500, } ); } });