// ============================================= // ASAAS — Integração com gateway de pagamentos // ============================================= const ASAAS_API_URL = process.env.ASAAS_ENV === "production" ? "https://www.asaas.com/api/v3" : "https://sandbox.asaas.com/api/v3" const ASAAS_API_KEY = process.env.ASAAS_API_KEY! async function asaasFetch(path: string, options: RequestInit = {}): Promise { const res = await fetch(`${ASAAS_API_URL}${path}`, { ...options, headers: { "Content-Type": "application/json", access_token: ASAAS_API_KEY, ...options.headers, }, }) if (!res.ok) { const error = await res.json().catch(() => ({})) throw new Error(`ASAAS API error ${res.status}: ${JSON.stringify(error)}`) } return res.json() } // ============================================= // CLIENTES // ============================================= export type AsaasCliente = { id: string name: string email: string phone?: string cpfCnpj?: string } export async function criarClienteAsaas(data: { nome: string email: string cpfCnpj: string telefone?: string }): Promise { return asaasFetch("/customers", { method: "POST", body: JSON.stringify({ name: data.nome, email: data.email, cpfCnpj: data.cpfCnpj, phone: data.telefone, }), }) } export async function buscarOuCriarClienteAsaas(data: { nome: string email: string cpfCnpj: string telefone?: string }): Promise { // Tenta encontrar cliente existente por CPF/CNPJ const lista = await asaasFetch<{ data: AsaasCliente[] }>( `/customers?cpfCnpj=${data.cpfCnpj}&limit=1` ) if (lista.data.length > 0) return lista.data[0] return criarClienteAsaas(data) } // ============================================= // PAGAMENTOS // ============================================= export type MetodoPagamento = "PIX" | "BOLETO" | "CREDIT_CARD" export type AsaasPaymentInput = { asaasClienteId: string valor: number // em reais (ex: 199.00) descricao: string metodo: MetodoPagamento vencimento?: string // YYYY-MM-DD, obrigatório para boleto cartao?: { holderName: string number: string expiryMonth: string expiryYear: string ccv: string } cartaoHolder?: { name: string email: string cpfCnpj: string postalCode: string addressNumber: string phone: string } } export type AsaasPayment = { id: string status: string value: number netValue: number billingType: string invoiceUrl?: string bankSlipUrl?: string pixQrCode?: { encodedImage: string payload: string expirationDate: string } } export async function criarPagamentoAsaas(input: AsaasPaymentInput): Promise { const vencimento = input.vencimento ?? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10) const body: Record = { customer: input.asaasClienteId, billingType: input.metodo, value: input.valor, dueDate: vencimento, description: input.descricao, } if (input.metodo === "CREDIT_CARD" && input.cartao && input.cartaoHolder) { body.creditCard = { holderName: input.cartao.holderName, number: input.cartao.number, expiryMonth: input.cartao.expiryMonth, expiryYear: input.cartao.expiryYear, ccv: input.cartao.ccv, } body.creditCardHolderInfo = { name: input.cartaoHolder.name, email: input.cartaoHolder.email, cpfCnpj: input.cartaoHolder.cpfCnpj, postalCode: input.cartaoHolder.postalCode, addressNumber: input.cartaoHolder.addressNumber, phone: input.cartaoHolder.phone, } } const payment = await asaasFetch("/payments", { method: "POST", body: JSON.stringify(body), }) // Para PIX, busca o QR Code if (input.metodo === "PIX") { const pix = await asaasFetch<{ encodedImage: string; payload: string; expirationDate: string }>( `/payments/${payment.id}/pixQrCode` ) payment.pixQrCode = pix } return payment } export async function buscarPagamentoAsaas(paymentId: string): Promise { return asaasFetch(`/payments/${paymentId}`) } // ============================================= // WEBHOOK — valida e processa evento // ============================================= export type AsaasWebhookEvent = { event: string payment: { id: string status: string value: number billingType: string paymentDate?: string } } export function parseWebhookPayload(body: unknown): AsaasWebhookEvent { return body as AsaasWebhookEvent } // Mapeia status ASAAS → status interno export function mapearStatus( asaasStatus: string ): "PENDING" | "RECEIVED" | "CONFIRMED" | "OVERDUE" | "REFUNDED" | "CANCELLED" { const map: Record = { PENDING: "PENDING", RECEIVED: "RECEIVED", CONFIRMED: "CONFIRMED", OVERDUE: "OVERDUE", REFUND_REQUESTED: "REFUNDED", REFUNDED: "REFUNDED", CHARGEBACK_REQUESTED: "CANCELLED", CHARGEBACK_DISPUTE: "CANCELLED", AWAITING_CHARGEBACK_REVERSAL: "CANCELLED", DUNNING_REQUESTED: "OVERDUE", DUNNING_RECEIVED: "RECEIVED", AWAITING_RISK_ANALYSIS: "PENDING", } return map[asaasStatus] ?? "PENDING" }