/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: twilio-whatsapp-webhook |-------------------------------------------------------------------------- | Recebe callbacks de status do Twilio para mensagens WhatsApp enviadas | pelas subcontas de cada tenant. | | URL configurada no número de cada subconta: | https://.supabase.co/functions/v1/twilio-whatsapp-webhook?tenant_id= | | Eventos recebidos (MessageStatus): | queued, failed, sent, delivered, undelivered, read |-------------------------------------------------------------------------- */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', } // Mapeia status Twilio → status interno function mapStatus(twilioStatus: string): string | null { switch (twilioStatus) { case 'delivered': return 'delivered' case 'read': return 'read' case 'failed': case 'undelivered': return 'failed' case 'sent': return 'sent' default: return null } } Deno.serve(async (req: Request) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) // Twilio envia POST com form-encoded if (req.method !== 'POST') { return new Response('ok', { status: 200, headers: corsHeaders }) } try { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const url = new URL(req.url) const tenantId = url.searchParams.get('tenant_id') const formData = await req.formData() const messageSid = formData.get('MessageSid') as string const messageStatus = formData.get('MessageStatus') as string const to = formData.get('To') as string const errorCode = formData.get('ErrorCode') as string | null const errorMessage = formData.get('ErrorMessage') as string | null if (!messageSid || !messageStatus) { return new Response('ok', { status: 200, headers: corsHeaders }) } const internalStatus = mapStatus(messageStatus) if (!internalStatus) { // Status intermediário (queued, accepted, etc.) — ignora return new Response('ok', { status: 200, headers: corsHeaders }) } // Atualiza notification_logs pelo provider_message_id const updateData: Record = { provider_status: messageStatus, status: internalStatus, } if (internalStatus === 'delivered') { updateData.delivered_at = new Date().toISOString() } else if (internalStatus === 'read') { updateData.read_at = new Date().toISOString() updateData.delivered_at = new Date().toISOString() } else if (internalStatus === 'failed') { updateData.failed_at = new Date().toISOString() updateData.failure_reason = errorMessage ? `${errorCode}: ${errorMessage}` : `Twilio status: ${messageStatus}` } const query = supabase .from('notification_logs') .update(updateData) .eq('provider_message_id', messageSid) if (tenantId) query.eq('tenant_id', tenantId) const { error } = await query if (error) { console.error('[webhook] Erro ao atualizar log:', error.message) } else { console.log(`[webhook] ${messageSid} → ${messageStatus} (tenant: ${tenantId ?? 'unknown'})`) } // Twilio espera 200 TwiML vazio ou texto simples return new Response('', { status: 200, headers: { ...corsHeaders, 'Content-Type': 'text/xml' }, }) } catch (e) { console.error('[webhook] Erro:', e) return new Response('ok', { status: 200, headers: corsHeaders }) } })