feat: recibo em PDF profissional e correção de duplicidade de mensagens
This commit is contained in:
parent
aadcf02cf2
commit
87973065a0
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue