#!/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 = `há ${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); });