feat: recibo em PDF profissional e correção de duplicidade de mensagens

This commit is contained in:
Sidney 2026-05-11 19:00:00 -03:00
parent aadcf02cf2
commit 87973065a0
1 changed files with 64 additions and 13 deletions

View File

@ -12,7 +12,9 @@
*/ */
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import path from 'path'; import { jsPDF } from 'jspdf';
import 'jspdf-autotable';
import fetch from 'node-fetch';
import fs from 'fs'; import fs from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import multer from 'multer'; import multer from 'multer';
@ -586,16 +588,56 @@ async function sendEvolutionMessage(asaasPaymentId, eventType, fallbackValorArg
.replace(/{link_boleto}/g, pdfUrl) .replace(/{link_boleto}/g, pdfUrl)
.replace(/{descricao}/g, descricao); .replace(/{descricao}/g, descricao);
const isTextOnlyEvent = ['PAYMENT_RECEIVED', 'PAYMENT_CONFIRMED', 'PAYMENT_DELETED'].includes(eventType);
const isPaymentConfirmation = ['PAYMENT_RECEIVED', 'PAYMENT_CONFIRMED'].includes(eventType); const isPaymentConfirmation = ['PAYMENT_RECEIVED', 'PAYMENT_CONFIRMED'].includes(eventType);
const isCreationEvent = eventType === 'PAYMENT_CREATED'; const isCreationEvent = eventType === 'PAYMENT_CREATED';
if (isPaymentConfirmation && pdfUrl && !templateText.includes('{link_boleto}')) { // 1. GERAÇÃO DE PDF (Recibo ou Boleto)
msgFinal += `\n\n📄 Acesse seu comprovante aqui:\n${pdfUrl}`;
}
let base64Pdf = null; let base64Pdf = null;
if (pdfUrl && !isTextOnlyEvent) { let fileName = `Documento-${targetName.replace(/\s+/g, '')}.pdf`;
if (isPaymentConfirmation) {
// GERAÇÃO DE RECIBO PROFISSIONAL (BACKEND - NODE COMPATIBLE)
try {
const doc = new jsPDF();
const profile = appData.profile || {};
// Moldura e Cabeçalho
doc.setDrawColor(0);
doc.setLineWidth(0.5);
doc.rect(10, 10, 190, 100);
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(profile.name || 'EduManager School', 105, 25, { align: 'center' });
doc.setFontSize(9);
doc.setFont('helvetica', 'normal');
doc.text(`CNPJ: ${profile.cnpj || '---'} | Contato: ${profile.phone || ''}`, 105, 32, { align: 'center' });
doc.setFontSize(16);
doc.text('RECIBO DE PAGAMENTO', 105, 50, { align: 'center' });
doc.setFontSize(11);
doc.text(`Recebemos de: ${aluno.name || aluno.nome}`, 20, 65);
doc.text(`A quantia de: R$ ${valorFormatado}`, 20, 75);
doc.text(`Referente a: ${descricao}`, 20, 85);
const dataHj = new Date().toLocaleDateString('pt-BR');
const dataPagamento = fallbackVencimento ? formatCobrancaDate(typeof fallbackVencimento === 'string' ? fallbackVencimento : dataHj) : dataHj;
doc.text(`Data do Pagamento: ${dataPagamento}`, 20, 95);
doc.line(60, 105, 150, 105);
doc.setFontSize(8);
doc.text('Autenticação Digital EduManager', 105, 108, { align: 'center' });
const pdfArrayBuffer = doc.output('arraybuffer');
base64Pdf = Buffer.from(pdfArrayBuffer).toString('base64');
fileName = `Recibo-${targetName.replace(/\s+/g, '')}.pdf`;
} catch (pdfErr) {
console.error('[WhatsApp] Erro ao gerar PDF de recibo:', pdfErr.message);
}
} else if (pdfUrl && !['PAYMENT_DELETED'].includes(eventType)) {
// TENTA BUSCAR PDF EXTERNO (BOLETO/CARNÊ)
for (let attempt = 1; attempt <= 3; attempt++) { for (let attempt = 1; attempt <= 3; attempt++) {
try { try {
const fetchOptions = { headers: { 'Accept': 'application/pdf' } }; const fetchOptions = { headers: { 'Accept': 'application/pdf' } };
@ -614,7 +656,9 @@ async function sendEvolutionMessage(asaasPaymentId, eventType, fallbackValorArg
} }
if ((isCreationEvent || isPaymentConfirmation || eventType === 'PAYMENT_UPDATED' || eventType === 'PAYMENT_UPCOMING') && !base64Pdf && pdfUrl) { if ((isCreationEvent || isPaymentConfirmation || eventType === 'PAYMENT_UPDATED' || eventType === 'PAYMENT_UPCOMING') && !base64Pdf && pdfUrl) {
msgFinal += `\n\n📄 Acesse aqui sua cobrança:\n${pdfUrl}`; if (!templateText.includes('{link_boleto}')) {
msgFinal += `\n\n📄 Acesse aqui seu documento:\n${pdfUrl}`;
}
} }
let endpoint = 'sendText'; let endpoint = 'sendText';
@ -622,10 +666,16 @@ async function sendEvolutionMessage(asaasPaymentId, eventType, fallbackValorArg
if (base64Pdf) { if (base64Pdf) {
endpoint = 'sendMedia'; endpoint = 'sendMedia';
let fileName = `Boleto-${targetName.replace(/\s+/g, '')}.pdf`;
if (isCarneCompleto) fileName = `Carne-${targetName.replace(/\s+/g, '')}.pdf`; if (isCarneCompleto) fileName = `Carne-${targetName.replace(/\s+/g, '')}.pdf`;
if (isPaymentConfirmation) fileName = `Comprovante-${targetName.replace(/\s+/g, '')}.pdf`; payload = {
payload = { number: cleanPhone, options: { delay: 1200, presence: "composing" }, mediatype: "document", mimetype: "application/pdf", fileName, media: base64Pdf, caption: msgFinal }; number: cleanPhone,
options: { delay: 1200, presence: "composing" },
mediatype: "document",
mimetype: "application/pdf",
fileName,
media: base64Pdf,
caption: msgFinal
};
} else { } else {
payload = { number: cleanPhone, text: msgFinal }; payload = { number: cleanPhone, text: msgFinal };
} }
@ -634,10 +684,10 @@ async function sendEvolutionMessage(asaasPaymentId, eventType, fallbackValorArg
const sendResp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify(payload) }); const sendResp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': evoConfig.apiKey }, body: JSON.stringify(payload) });
if (sendResp.ok) { if (sendResp.ok) {
console.log(`[WhatsApp] ✅ Enviado para ${cleanPhone}`); console.log(`[WhatsApp] ✅ Enviado para ${cleanPhone} (${eventType})`);
return true; return true;
} else { } else {
console.error(`[WhatsApp] ❌ Erro:`, sendResp.status); console.error(`[WhatsApp] ❌ Erro ao enviar WhatsApp:`, sendResp.status);
return false; return false;
} }
} catch (error) { } catch (error) {
@ -681,6 +731,7 @@ app.post('/api/webhook_asaas', async (req, res) => {
if (payload.payment.transactionReceiptUrl) { if (payload.payment.transactionReceiptUrl) {
updateData.transaction_receipt_url = payload.payment.transactionReceiptUrl; updateData.transaction_receipt_url = payload.payment.transactionReceiptUrl;
} }
// Chamada única: sendEvolutionMessage já possui trava interna de cache por ID de pagamento
sendEvolutionMessage(asaasPaymentId, 'PAYMENT_RECEIVED'); sendEvolutionMessage(asaasPaymentId, 'PAYMENT_RECEIVED');
break; break;