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>
425 lines
12 KiB
TypeScript
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("/")
|
|
}
|