chore: sincroniza projeto para gitea

This commit is contained in:
Tiago
2025-11-29 21:31:52 -03:00
parent 33d8645eb4
commit 7e7a0f8867
129 changed files with 24999 additions and 6757 deletions

View File

@@ -0,0 +1,326 @@
#!/usr/bin/env node
/**
* Script de Envio Automático de Alertas de Vencimento
*
* Este script deve ser executado diariamente às 09:00 (horário de Brasília)
* via cron job para enviar alertas de parcelas vencendo.
*
* Cron: 0 9 * * * /usr/bin/node /caminho/para/enviar-alertas-parcelas.js
*/
const { createClient } = require('@supabase/supabase-js');
const axios = require('axios');
// Configurações do Supabase
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://ydhzylfnpqlxnzfcclla.supabase.co';
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY;
if (!SUPABASE_KEY) {
console.error('❌ SUPABASE_KEY não configurado!');
process.exit(1);
}
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Função para formatar data no padrão brasileiro
function formatarDataBR(data) {
const d = new Date(data);
return d.toLocaleDateString('pt-BR', { timeZone: 'America/Sao_Paulo' });
}
// Função para formatar moeda
function formatarMoeda(valor) {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(valor);
}
// Função para calcular dias entre datas
function calcularDiasEntre(data1, data2) {
const umDia = 24 * 60 * 60 * 1000;
const d1 = new Date(data1);
const d2 = new Date(data2);
return Math.round((d2 - d1) / umDia);
}
// Função para enviar mensagem via Evolution API
async function enviarWhatsApp(telefone, mensagem, evolutionConfig) {
try {
const url = `${evolutionConfig.apiUrl}/message/sendText/${evolutionConfig.instanceName}`;
const response = await axios.post(url, {
number: telefone,
textMessage: {
text: mensagem
}
}, {
headers: {
'apikey': evolutionConfig.apiKey,
'Content-Type': 'application/json'
}
});
return { success: true, data: response.data };
} catch (error) {
console.error(`❌ Erro ao enviar WhatsApp para ${telefone}:`, error.message);
return { success: false, error: error.message };
}
}
// Função para gerar PIX via Mercado Pago
async function gerarPix(parcela, mercadoPagoToken) {
try {
const response = await axios.post('https://api.mercadopago.com/v1/payments', {
transaction_amount: parseFloat(parcela.valor),
description: `Parcela ${parcela.numero_parcela} - Venda #${parcela.venda_id}`,
payment_method_id: 'pix',
payer: {
email: 'cliente@liberi.com.br',
identification: {
type: 'CPF',
number: '00000000000'
}
}
}, {
headers: {
'Authorization': `Bearer ${mercadoPagoToken}`,
'Content-Type': 'application/json'
}
});
return {
success: true,
qrCode: response.data.point_of_interaction?.transaction_data?.qr_code,
qrCodeBase64: response.data.point_of_interaction?.transaction_data?.qr_code_base64,
paymentId: response.data.id
};
} catch (error) {
console.error(`❌ Erro ao gerar PIX para parcela ${parcela.id}:`, error.message);
return { success: false, error: error.message };
}
}
// Função principal
async function enviarAlertasVencimento() {
console.log('\n🕐 Iniciando envio de alertas de vencimento...');
console.log(`⏰ Horário: ${new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}\n`);
try {
// 1. Buscar configurações WhatsApp
const { data: configData } = await supabase
.from('configuracoes')
.select('chave, valor')
.in('chave', [
'whatsapp_primeiro_alerta_dias',
'whatsapp_segundo_alerta_dias',
'whatsapp_alerta_apos_vencimento_dias',
'whatsapp_primeiro_alerta_ativo',
'whatsapp_segundo_alerta_ativo',
'whatsapp_alerta_apos_vencimento_ativo',
'whatsapp_primeiro_alerta_mensagem',
'whatsapp_segundo_alerta_mensagem',
'whatsapp_alerta_apos_vencimento_mensagem',
'evolution_api_url',
'evolution_instance_name',
'evolution_api_key',
'mercadopago_access_token'
]);
const config = {};
configData?.forEach(item => {
config[item.chave] = item.valor;
});
// Configurações padrão
const primeiroAlertaDias = parseInt(config.whatsapp_primeiro_alerta_dias) || 3;
const segundoAlertaDias = parseInt(config.whatsapp_segundo_alerta_dias) || 0;
const alertaAposVencimentoDias = parseInt(config.whatsapp_alerta_apos_vencimento_dias) || 3;
const primeiroAlertas = config.whatsapp_primeiro_alerta_ativo === 'true';
const segundoAlertas = config.whatsapp_segundo_alerta_ativo === 'true';
const alertaAposVencimento = config.whatsapp_alerta_apos_vencimento_ativo === 'true';
const evolutionConfig = {
apiUrl: config.evolution_api_url,
instanceName: config.evolution_instance_name,
apiKey: config.evolution_api_key
};
const mercadoPagoToken = config.mercadopago_access_token;
console.log('📋 Configurações carregadas:');
console.log(` - Primeiro alerta: ${primeiroAlertaDias} dias antes (${primeiroAlertas ? 'ATIVO' : 'INATIVO'})`);
console.log(` - Segundo alerta: ${segundoAlertaDias} dias antes (${segundoAlertas ? 'ATIVO' : 'INATIVO'})`);
console.log(` - Alerta pós-vencimento: ${alertaAposVencimentoDias} dias após (${alertaAposVencimento ? 'ATIVO' : 'INATIVO'})\n`);
// 2. Buscar parcelas pendentes
const { data: parcelas, error } = await supabase
.from('venda_parcelas')
.select(`
*,
vendas (
id_venda,
cliente_id,
clientes (
nome_completo,
whatsapp
)
)
`)
.eq('status', 'pendente')
.order('data_vencimento', { ascending: true });
if (error) {
throw error;
}
if (!parcelas || parcelas.length === 0) {
console.log(' Nenhuma parcela pendente encontrada.\n');
return;
}
console.log(`📦 ${parcelas.length} parcela(s) pendente(s) encontrada(s)\n`);
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
let alertasEnviados = 0;
let erros = 0;
// 3. Processar cada parcela
for (const parcela of parcelas) {
const dataVencimento = new Date(parcela.data_vencimento);
dataVencimento.setHours(0, 0, 0, 0);
const diasParaVencimento = calcularDiasEntre(hoje, dataVencimento);
const cliente = parcela.vendas?.clientes;
if (!cliente || !cliente.whatsapp) {
console.log(`⚠️ Parcela ${parcela.numero_parcela} (Venda #${parcela.vendas?.id_venda}): Cliente sem WhatsApp`);
continue;
}
let deveEnviar = false;
let tipoAlerta = '';
let mensagemTemplate = '';
// Verificar qual tipo de alerta enviar
if (diasParaVencimento === primeiroAlertaDias && primeiroAlertas) {
deveEnviar = true;
tipoAlerta = 'primeiro_alerta';
mensagemTemplate = config.whatsapp_primeiro_alerta_mensagem ||
'Olá {cliente}! 👋\n\nLembramos que você tem uma parcela no valor de {valor} com vencimento {quando}.\n\nAgradecemos a atenção!';
} else if (diasParaVencimento === segundoAlertaDias && segundoAlertas) {
deveEnviar = true;
tipoAlerta = 'segundo_alerta';
mensagemTemplate = config.whatsapp_segundo_alerta_mensagem ||
'Olá {cliente}! 👋\n\nSua parcela de {valor} vence {quando}.\n\n📱 Gerando PIX para facilitar o pagamento...';
} else if (diasParaVencimento < 0 && Math.abs(diasParaVencimento) === alertaAposVencimentoDias && alertaAposVencimento) {
deveEnviar = true;
tipoAlerta = 'alerta_apos_vencimento';
mensagemTemplate = config.whatsapp_alerta_apos_vencimento_mensagem ||
'Olá {cliente}! 👋\n\nIdentificamos que a parcela {parcela} no valor de {valor} venceu {quando}.\n\nPor favor, regularize o pagamento.';
}
if (!deveEnviar) {
continue;
}
// Preparar dados da mensagem
const nomeCliente = cliente.nome_completo.split(' ')[0];
const valorFormatado = formatarMoeda(parcela.valor);
const dataVencFormatada = formatarDataBR(parcela.data_vencimento);
let quando = '';
if (diasParaVencimento > 0) {
quando = diasParaVencimento === 1 ? 'amanhã' : `em ${diasParaVencimento} dias (${dataVencFormatada})`;
} else if (diasParaVencimento === 0) {
quando = 'hoje';
} else {
quando = `${Math.abs(diasParaVencimento)} dias (${dataVencFormatada})`;
}
// Substituir variáveis na mensagem
let mensagem = mensagemTemplate
.replace(/{cliente}/g, nomeCliente)
.replace(/{valor}/g, valorFormatado)
.replace(/{quando}/g, quando)
.replace(/{parcela}/g, `${parcela.numero_parcela}/${parcela.vendas?.parcelas || '?'}`);
// Se for no dia do vencimento, gerar PIX
if (tipoAlerta === 'segundo_alerta' && mercadoPagoToken) {
console.log(`🔄 Gerando PIX para parcela ${parcela.numero_parcela}...`);
const pixResult = await gerarPix(parcela, mercadoPagoToken);
if (pixResult.success && pixResult.qrCode) {
mensagem += `\n\n📱 *PIX Copia e Cola:*\n\`\`\`${pixResult.qrCode}\`\`\``;
// Atualizar parcela com dados do PIX
await supabase
.from('venda_parcelas')
.update({
pix_payment_id: pixResult.paymentId,
pix_qr_code: pixResult.qrCode,
pix_qr_code_base64: pixResult.qrCodeBase64
})
.eq('id', parcela.id);
}
}
// Enviar mensagem
console.log(`📤 Enviando ${tipoAlerta} para ${cliente.nome_completo} (${cliente.whatsapp})...`);
const resultado = await enviarWhatsApp(cliente.whatsapp, mensagem, evolutionConfig);
if (resultado.success) {
alertasEnviados++;
console.log(` ✅ Enviado com sucesso!\n`);
// Registrar no histórico
await supabase
.from('mensagens_whatsapp')
.insert({
telefone_cliente: cliente.whatsapp,
cliente_nome: cliente.nome_completo,
mensagem: mensagem,
tipo: 'enviada',
status: 'enviada'
});
} else {
erros++;
console.log(` ❌ Falha no envio: ${resultado.error}\n`);
}
// Pequeno delay entre mensagens
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Resumo
console.log('\n' + '='.repeat(50));
console.log('📊 RESUMO DO ENVIO');
console.log('='.repeat(50));
console.log(`✅ Alertas enviados: ${alertasEnviados}`);
console.log(`❌ Erros: ${erros}`);
console.log(`📦 Total de parcelas verificadas: ${parcelas.length}`);
console.log('='.repeat(50) + '\n');
} catch (error) {
console.error('\n💥 Erro fatal:', error);
process.exit(1);
}
}
// Executar
enviarAlertasVencimento()
.then(() => {
console.log('✅ Script finalizado com sucesso!');
process.exit(0);
})
.catch((error) => {
console.error('💥 Erro fatal:', error);
process.exit(1);
});