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:
207
lib/asaas.ts
Normal file
207
lib/asaas.ts
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user