Files
asaas-checkout/app/api/asaas-webhook/route.ts
Felipe Carvalho 038ce3f556 feat: initial commit — asaas-checkout template white-label
Template genérico de checkout com ASAAS, parametrizado via env vars.
Inclui fluxo completo: checkout → pedido → polling → webhook → admin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 06:40:41 +02:00

103 lines
3.5 KiB
TypeScript

import { NextResponse } from "next/server"
import { createServiceClient } from "@/lib/supabase"
import { parseWebhookPayload, mapearStatus } from "@/lib/asaas"
const N8N_WEBHOOK_URL = process.env.N8N_WEBHOOK_URL ?? ""
export async function POST(request: Request) {
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 })
}
const supabase = createServiceClient()
// Salva log do webhook para auditoria
await supabase.from("webhook_logs").insert({
evento: (body as { event?: string }).event ?? "unknown",
payload: body,
processado: false,
})
try {
const event = parseWebhookPayload(body)
// Processa apenas eventos de pagamento
if (!event.event.startsWith("PAYMENT_")) {
return NextResponse.json({ ok: true })
}
const novoStatus = mapearStatus(event.payment.status)
const isPago =
event.payment.status === "RECEIVED" || event.payment.status === "CONFIRMED"
const paidAt = isPago ? new Date().toISOString() : null
const updateData: Record<string, unknown> = { status: novoStatus }
if (paidAt) updateData.paid_at = paidAt
// Atualiza pedido e busca dados completos para notificação
const { data: pedido } = await supabase
.from("pedidos")
.update(updateData)
.eq("asaas_payment_id", event.payment.id)
.select("id, valor_centavos, metodo_pagamento, cliente_id, produto_id")
.single()
if (pedido) {
// Marca webhook como processado
await supabase
.from("webhook_logs")
.update({ processado: true })
.eq("evento", event.event)
.order("created_at", { ascending: false })
.limit(1)
// Dispara automação n8n apenas em pagamentos confirmados
if (isPago && N8N_WEBHOOK_URL) {
// Busca cliente e produto para enriquecer o payload
const [{ data: cliente }, { data: produto }] = await Promise.all([
supabase.from("clientes").select("nome, email, telefone, cpf_cnpj").eq("id", pedido.cliente_id).single(),
supabase.from("produtos").select("nome, tipo, validade, midia").eq("id", pedido.produto_id).single(),
])
const notificacao = {
evento: event.event,
pedido_id: pedido.id,
asaas_payment_id: event.payment.id,
valor: (pedido.valor_centavos / 100).toLocaleString("pt-BR", { minimumFractionDigits: 2, style: "currency", currency: "BRL" }),
metodo: pedido.metodo_pagamento,
cliente: {
nome: cliente?.nome ?? "—",
email: cliente?.email ?? "—",
telefone: cliente?.telefone ?? "—",
cpf_cnpj: cliente?.cpf_cnpj ?? "—",
},
produto: {
nome: produto?.nome ?? "—",
tipo: produto?.tipo ?? "—",
validade: produto?.validade ?? "—",
midia: produto?.midia ?? "—",
},
pago_em: paidAt,
link_agendamento: process.env.NEXT_PUBLIC_AFTER_PAYMENT_REDIRECT ?? "/",
}
// Fire-and-forget — não bloqueia a resposta ao ASAAS
fetch(N8N_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(notificacao),
}).catch((err) => console.error("n8n webhook error:", err))
}
}
return NextResponse.json({ ok: true })
} catch (error) {
console.error("Webhook processing error:", error)
return NextResponse.json({ error: "Processing failed" }, { status: 500 })
}
}