import { type NextRequest, NextResponse } from "next/server"; import { markAsPaid } from "@/lib/payment-cache"; import { checkRateLimit, createRateLimitResponse, getClientIP, } from "@/lib/rate-limiter"; import { secureLogger } from "@/lib/secure-logger"; // Whitelist de IPs do AbacatePay (configurável via env) const ABACATEPAY_IPS = process.env.ABACATEPAY_WHITELIST_IPS?.split(",") || []; export async function POST(request: NextRequest) { try { // Rate limiting const ip = getClientIP(request); const rateLimit = checkRateLimit(ip, "/api/webhooks/abacatepay"); if (!rateLimit.allowed) { return createRateLimitResponse(rateLimit.resetAt); } // Validar IP de origem (se configurado) if (ABACATEPAY_IPS.length > 0 && !ABACATEPAY_IPS.includes(ip)) { secureLogger.warn("Webhook rejeitado: IP não autorizado", { ip }); return NextResponse.json({ error: "IP não autorizado" }, { status: 403 }); } const payload = await request.text(); // AbacatePay envia webhookSecret como query param const { searchParams } = new URL(request.url); const webhookSecret = searchParams.get("webhookSecret"); const expectedSecret = process.env.ABACATEPAY_WEBHOOK_SECRET; // Validar secret if (!webhookSecret || !expectedSecret || webhookSecret !== expectedSecret) { secureLogger.warn("Webhook rejeitado: secret inválido ou ausente"); return NextResponse.json({ error: "Secret inválido" }, { status: 401 }); } secureLogger.info("Webhook autenticado com sucesso"); const event = JSON.parse(payload); secureLogger.debug("Webhook recebido", event); // Validar timestamp do evento (prevenir replay attacks) const eventTimestamp = event.timestamp || event.data?.pixQrCode?.createdAt; if (eventTimestamp) { const eventTime = new Date(eventTimestamp).getTime(); const now = Date.now(); const timeDiff = Math.abs(now - eventTime); // Rejeitar eventos com mais de 5 minutos de diferença if (timeDiff > 5 * 60 * 1000) { secureLogger.warn("Webhook rejeitado: timestamp inválido", { timeDiff: `${timeDiff / 1000}s`, }); return NextResponse.json( { error: "Timestamp inválido" }, { status: 400 }, ); } } // Verificar se é evento de pagamento confirmado const isPaidEvent = event.event === "billing.paid" || event.event === "pix.paid" || event.data?.pixQrCode?.status === "PAID"; if (isPaidEvent) { // Extrair dados do PIX const pixQrCode = event.data.pixQrCode; const payment = event.data.payment; if (!pixQrCode || pixQrCode.status !== "PAID") { secureLogger.info("Webhook recebido mas status não é PAID"); return NextResponse.json({ success: true }); } secureLogger.info("Doação confirmada via webhook", { pixId: pixQrCode.id, amount: pixQrCode.amount, }); // Detectar se é doação anônima ou identificada const isAnonymous = !pixQrCode.customer; // Log estruturado para futura persistência em banco de dados const donationRecord: any = { type: "donation_paid", pixId: pixQrCode.id, amount: pixQrCode.amount, fee: payment?.fee, method: payment?.method, anonymous: isAnonymous, paidAt: new Date().toISOString(), createdAt: new Date().toISOString(), devMode: event.devMode || false, }; // Adicionar dados do customer se disponível (doação identificada) if (!isAnonymous && pixQrCode.customer?.metadata) { donationRecord.customer = { id: pixQrCode.customer.id, name: pixQrCode.customer.metadata.name, email: pixQrCode.customer.metadata.email, cellphone: pixQrCode.customer.metadata.cellphone, taxId: pixQrCode.customer.metadata.taxId, }; secureLogger.info("Email disponível para agradecimento", { maskedEmail: pixQrCode.customer.metadata.email, }); } secureLogger.info( "Registro de pagamento criado", secureLogger.mask(donationRecord), ); // Salvar no cache para frontend detectar via polling markAsPaid(pixQrCode.id, donationRecord); // TODO: Quando implementar banco de dados, salvar aqui: // await db.donations.create(donationRecord); // TODO: Quando implementar email, enviar aqui: // if (!isAnonymous && donationRecord.customer?.email) { // await sendThankYouEmail({ // to: donationRecord.customer.email, // donorName: donationRecord.customer.name, // amount: donationRecord.amount, // }); // } // TODO: Atualizar estatísticas // await updateDonationStats(donationRecord.amount); } else { secureLogger.info("Evento de webhook (não é pagamento)", { event: event.event, }); } return NextResponse.json({ success: true }); } catch (error) { secureLogger.error("Webhook error", error); return NextResponse.json( { error: "Erro ao processar webhook" }, { status: 500 }, ); } }