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>
This commit is contained in:
102
app/api/asaas-webhook/route.ts
Normal file
102
app/api/asaas-webhook/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user