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:
2026-04-16 06:40:41 +02:00
commit 038ce3f556
103 changed files with 20709 additions and 0 deletions

207
lib/asaas.ts Normal file
View File

@@ -0,0 +1,207 @@
// =============================================
// 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<T>(path: string, options: RequestInit = {}): Promise<T> {
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<AsaasCliente> {
return asaasFetch<AsaasCliente>("/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<AsaasCliente> {
// 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<AsaasPayment> {
const vencimento =
input.vencimento ?? new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
const body: Record<string, unknown> = {
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<AsaasPayment>("/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<AsaasPayment> {
return asaasFetch<AsaasPayment>(`/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<string, "PENDING" | "RECEIVED" | "CONFIRMED" | "OVERDUE" | "REFUNDED" | "CANCELLED"> = {
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"
}