/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: asaas-webhook |-------------------------------------------------------------------------- | Recebe eventos do Asaas. Atualiza status da purchase e credita saldo | quando PAYMENT_RECEIVED / PAYMENT_CONFIRMED. | | Config no Asaas: | Dashboard → Integrações → Webhooks → URL: | https://.supabase.co/functions/v1/asaas-webhook | Token de autenticação (opcional): setar em ASAAS_WEBHOOK_TOKEN | | Eventos tratados: | PAYMENT_RECEIVED → credita saldo (pago) | PAYMENT_CONFIRMED → idem (garantia adicional) | PAYMENT_OVERDUE → marca como expired | PAYMENT_DELETED → marca como cancelled | PAYMENT_REFUNDED → marca como refunded + estorna (não implementado auto) |-------------------------------------------------------------------------- */ 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, asaas-access-token', 'Access-Control-Allow-Methods': 'POST, OPTIONS', } function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } Deno.serve(async (req: Request) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) if (req.method !== 'POST') return json({ ok: false, error: 'method_not_allowed' }, 405) // Valida token (se configurado) const expectedToken = Deno.env.get('ASAAS_WEBHOOK_TOKEN') || '' if (expectedToken) { const gotToken = req.headers.get('asaas-access-token') || '' if (gotToken !== expectedToken) { console.warn('[asaas-webhook] token mismatch') return json({ ok: false, error: 'forbidden' }, 403) } } try { const payload = await req.json().catch(() => null) as Record | null if (!payload) return json({ ok: true, skipped: 'invalid_payload' }) const event = String(payload.event || '') const payment = (payload.payment as Record) || {} const paymentId = String(payment.id || '') const externalRef = String(payment.externalReference || '') if (!paymentId) return json({ ok: true, skipped: 'no_payment_id' }) const supa = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) // Localiza purchase (prefere externalReference = purchase.id) let purchase: Record | null = null if (externalRef) { const { data } = await supa .from('whatsapp_credit_purchases') .select('id, tenant_id, credits, status') .eq('id', externalRef) .maybeSingle() purchase = data } if (!purchase) { const { data } = await supa .from('whatsapp_credit_purchases') .select('id, tenant_id, credits, status') .eq('asaas_payment_id', paymentId) .maybeSingle() purchase = data } if (!purchase) { console.warn('[asaas-webhook] purchase not found for payment:', paymentId) return json({ ok: true, skipped: 'purchase_not_found' }) } const now = new Date().toISOString() switch (event) { case 'PAYMENT_RECEIVED': case 'PAYMENT_CONFIRMED': case 'PAYMENT_RECEIVED_IN_CASH_UNDONE': // Credita saldo apenas se ainda não foi processado if (purchase.status !== 'paid') { // Atualiza status await supa .from('whatsapp_credit_purchases') .update({ status: 'paid', paid_at: now }) .eq('id', purchase.id as string) // Credita saldo via RPC const { error: rpcErr } = await supa.rpc('add_whatsapp_credits', { p_tenant_id: purchase.tenant_id as string, p_amount: purchase.credits as number, p_kind: 'purchase', p_purchase_id: purchase.id as string, p_note: `Pagamento Asaas confirmado — ${paymentId}` }) if (rpcErr) { console.error('[asaas-webhook] add_credits error:', rpcErr.message) } } break case 'PAYMENT_OVERDUE': if (purchase.status === 'pending') { await supa.from('whatsapp_credit_purchases').update({ status: 'expired' }).eq('id', purchase.id as string) } break case 'PAYMENT_DELETED': await supa.from('whatsapp_credit_purchases').update({ status: 'cancelled' }).eq('id', purchase.id as string) break case 'PAYMENT_REFUNDED': await supa.from('whatsapp_credit_purchases').update({ status: 'refunded' }).eq('id', purchase.id as string) // Não estorna saldo automaticamente — admin decide (pode ter consumido) break default: console.log('[asaas-webhook] unhandled event:', event) } return json({ ok: true, event, purchase_id: purchase.id }) } catch (err) { console.error('[asaas-webhook] fatal:', err) return json({ ok: false, error: String(err) }, 500) } })