Files
asaas-checkout/lib/actions.ts
Felipe Carvalho 038ce3f556 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>
2026-04-16 06:40:41 +02:00

425 lines
12 KiB
TypeScript

"use server"
import { createServiceClient } from "@/lib/supabase"
import { buscarOuCriarClienteAsaas, criarPagamentoAsaas, type MetodoPagamento } from "@/lib/asaas"
import { revalidatePath } from "next/cache"
// =============================================
// PRODUTOS
// =============================================
export async function getProdutos() {
const supabase = createServiceClient()
const { data, error } = await supabase
.from("produtos")
.select("*")
.eq("ativo", true)
.order("preco_centavos", { ascending: true })
if (error) throw new Error(error.message)
return data
}
export async function getProdutoById(id: string) {
const supabase = createServiceClient()
const { data, error } = await supabase
.from("produtos")
.select("*")
.eq("id", id)
.single()
if (error) throw new Error(error.message)
return data
}
export async function createProduto(data: {
nome: string
descricao?: string
tipo: "PF" | "PJ" | "SSL" | "NFe"
validade: string
midia?: string
preco_centavos: number
imagem_url?: string
}) {
const supabase = createServiceClient()
const { error } = await supabase.from("produtos").insert(data)
if (error) throw new Error(error.message)
revalidatePath("/admin")
revalidatePath("/produtos")
return { success: true }
}
export async function updateProduto(
id: string,
data: Partial<{
nome: string
descricao: string
tipo: "PF" | "PJ" | "SSL" | "NFe"
validade: string
midia: string
preco_centavos: number
ativo: boolean
imagem_url: string
}>
) {
const supabase = createServiceClient()
const { error } = await supabase.from("produtos").update(data).eq("id", id)
if (error) throw new Error(error.message)
revalidatePath("/admin")
revalidatePath("/produtos")
return { success: true }
}
export async function deleteProduto(id: string) {
const supabase = createServiceClient()
const { error } = await supabase.from("produtos").update({ ativo: false }).eq("id", id)
if (error) throw new Error(error.message)
revalidatePath("/admin")
return { success: true }
}
// =============================================
// CLIENTES
// =============================================
export async function getClientes(options: { limit?: number; search?: string } = {}) {
const supabase = createServiceClient()
let query = supabase.from("clientes").select("*").order("created_at", { ascending: false })
if (options.search) {
query = query.or(
`nome.ilike.%${options.search}%,email.ilike.%${options.search}%,cpf_cnpj.ilike.%${options.search}%`
)
}
if (options.limit) query = query.limit(options.limit)
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
export async function createCliente(data: {
nome: string
email: string
telefone?: string
cpf_cnpj?: string
}) {
const supabase = createServiceClient()
const { data: cliente, error } = await supabase
.from("clientes")
.insert(data)
.select()
.single()
if (error) throw new Error(error.message)
revalidatePath("/admin")
return cliente
}
// =============================================
// CHECKOUT — cria cliente no ASAAS + pedido
// =============================================
export async function criarCheckout(input: {
produtoId: string
cliente: {
nome: string
email: string
cpfCnpj: string
telefone?: string
}
metodo: MetodoPagamento
cartao?: {
holderName: string
number: string
expiryMonth: string
expiryYear: string
ccv: string
postalCode: string
addressNumber: string
}
}) {
const supabase = createServiceClient()
// 1. Busca produto
const { data: produto, error: produtoError } = await supabase
.from("produtos")
.select("*")
.eq("id", input.produtoId)
.eq("ativo", true)
.single()
if (produtoError || !produto) throw new Error("Produto não encontrado")
// 2. Busca ou cria cliente no banco (por email ou CPF/CNPJ)
let cliente = await supabase
.from("clientes")
.select("*")
.or(`email.eq.${input.cliente.email},cpf_cnpj.eq.${input.cliente.cpfCnpj}`)
.limit(1)
.single()
.then(({ data }) => data)
if (!cliente) {
const { data: novoCliente, error } = await supabase
.from("clientes")
.upsert({
nome: input.cliente.nome,
email: input.cliente.email,
telefone: input.cliente.telefone,
cpf_cnpj: input.cliente.cpfCnpj,
}, { onConflict: "cpf_cnpj" })
.select()
.single()
if (error) throw new Error(error.message)
cliente = novoCliente
}
// 3. Busca ou cria cliente no ASAAS
const asaasCliente = await buscarOuCriarClienteAsaas({
nome: input.cliente.nome,
email: input.cliente.email,
cpfCnpj: input.cliente.cpfCnpj,
telefone: input.cliente.telefone,
})
// Atualiza asaas_id no banco se necessário
if (!cliente.asaas_id) {
await supabase.from("clientes").update({ asaas_id: asaasCliente.id }).eq("id", cliente.id)
}
// 4. Cria pagamento no ASAAS
const valorReais = produto.preco_centavos / 100
const asaasPayment = await criarPagamentoAsaas({
asaasClienteId: asaasCliente.id,
valor: valorReais,
descricao: `${produto.nome}`,
metodo: input.metodo,
cartao: input.cartao
? {
holderName: input.cartao.holderName,
number: input.cartao.number,
expiryMonth: input.cartao.expiryMonth,
expiryYear: input.cartao.expiryYear,
ccv: input.cartao.ccv,
}
: undefined,
cartaoHolder: input.cartao
? {
name: input.cliente.nome,
email: input.cliente.email,
cpfCnpj: input.cliente.cpfCnpj,
postalCode: input.cartao.postalCode,
addressNumber: input.cartao.addressNumber,
phone: input.cliente.telefone ?? "",
}
: undefined,
})
// 5. Salva pedido no Supabase
const { data: pedido, error: pedidoError } = await supabase
.from("pedidos")
.insert({
cliente_id: cliente.id,
produto_id: produto.id,
valor_centavos: produto.preco_centavos,
metodo_pagamento: input.metodo,
status: "PENDING",
asaas_payment_id: asaasPayment.id,
asaas_invoice_url: asaasPayment.invoiceUrl ?? asaasPayment.bankSlipUrl ?? null,
pix_copia_cola: asaasPayment.pixQrCode?.payload ?? null,
pix_qrcode_url: asaasPayment.pixQrCode?.encodedImage ?? null,
})
.select()
.single()
if (pedidoError) throw new Error(pedidoError.message)
// Dispara notificação PIX via n8n (fire-and-forget)
if (input.metodo === "PIX" && asaasPayment.pixQrCode?.payload) {
const n8nPixUrl = process.env.N8N_PIX_WEBHOOK_URL
if (n8nPixUrl) {
fetch(n8nPixUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pedido_id: pedido.id,
cliente: {
nome: input.cliente.nome,
email: input.cliente.email,
telefone: input.cliente.telefone ?? "",
},
produto: {
nome: produto.nome,
validade: produto.validade,
},
valor: (produto.preco_centavos / 100).toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 2,
}),
pix_copia_cola: asaasPayment.pixQrCode.payload,
pix_qrcode_base64: asaasPayment.pixQrCode.encodedImage,
}),
}).catch((err) => console.error("n8n pix webhook error:", err))
}
}
return {
pedidoId: pedido.id,
metodo: input.metodo,
invoiceUrl: asaasPayment.invoiceUrl ?? asaasPayment.bankSlipUrl,
pixCopiaECola: asaasPayment.pixQrCode?.payload,
pixQrCodeBase64: asaasPayment.pixQrCode?.encodedImage,
}
}
// =============================================
// PEDIDOS
// =============================================
export async function getPedidos(options: { limit?: number; status?: string } = {}) {
const supabase = createServiceClient()
let query = supabase
.from("pedidos")
.select("*, clientes(nome, email, telefone, cpf_cnpj), produtos(nome, tipo, validade, midia)")
.order("created_at", { ascending: false })
if (options.status) query = query.eq("status", options.status)
if (options.limit) query = query.limit(options.limit)
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
export async function getPedidoById(id: string) {
const supabase = createServiceClient()
const { data, error } = await supabase
.from("pedidos")
.select("*, clientes(*), produtos(*)")
.eq("id", id)
.single()
if (error) throw new Error(error.message)
return data
}
// =============================================
// AGENDAMENTOS
// =============================================
export async function getAgendamentos(options: { limit?: number } = {}) {
const supabase = createServiceClient()
let query = supabase
.from("agendamentos")
.select("*, clientes(nome, email), produtos(nome)")
.order("data_hora", { ascending: true })
if (options.limit) query = query.limit(options.limit)
const { data, error } = await query
if (error) throw new Error(error.message)
return data
}
export async function criarAgendamento(data: {
clienteId: string
produtoId: string
pedidoId?: string
dataHora: string
observacoes?: string
}) {
const supabase = createServiceClient()
const { data: agendamento, error } = await supabase
.from("agendamentos")
.insert({
cliente_id: data.clienteId,
produto_id: data.produtoId,
pedido_id: data.pedidoId,
data_hora: data.dataHora,
observacoes: data.observacoes,
})
.select()
.single()
if (error) throw new Error(error.message)
revalidatePath("/admin")
return agendamento
}
// =============================================
// CUPONS
// =============================================
export async function getCupomDestaque() {
const supabase = createServiceClient()
const hoje = new Date().toISOString().slice(0, 10)
const { data } = await supabase
.from("cupons")
.select("*")
.eq("ativo", true)
.eq("destaque", true)
.gte("validade", hoje)
.order("created_at", { ascending: false })
.limit(1)
.single()
return data ?? null
}
export async function getCupons() {
const supabase = createServiceClient()
const { data, error } = await supabase
.from("cupons")
.select("*")
.order("created_at", { ascending: false })
if (error) throw new Error(error.message)
return data
}
export async function criarCupom(input: {
codigo: string
descricao: string
percentual: number
validade: string
destaque: boolean
}) {
const supabase = createServiceClient()
const { error } = await supabase.from("cupons").insert({
codigo: input.codigo.toUpperCase(),
descricao: input.descricao,
percentual: input.percentual,
validade: input.validade,
ativo: true,
destaque: input.destaque,
})
if (error) throw new Error(error.message)
revalidatePath("/admin")
revalidatePath("/")
}
export async function atualizarCupom(id: string, input: Partial<{
codigo: string
descricao: string
percentual: number
validade: string
ativo: boolean
destaque: boolean
}>) {
const supabase = createServiceClient()
const { error } = await supabase.from("cupons").update(input).eq("id", id)
if (error) throw new Error(error.message)
revalidatePath("/admin")
revalidatePath("/")
}
export async function deletarCupom(id: string) {
const supabase = createServiceClient()
const { error } = await supabase.from("cupons").delete().eq("id", id)
if (error) throw new Error(error.message)
revalidatePath("/admin")
revalidatePath("/")
}