import { PDFDocument, StandardFonts, rgb, PageSizes } from 'pdf-lib'; import QRCode from 'qrcode'; import { format, isValid } from 'date-fns'; import ptBR from 'date-fns/locale/pt-BR'; import { prisma } from '../prismaClient'; import * as fs from 'fs/promises'; import * as path from 'path'; export class ReportGenerator { async generateReport(data: { file: Express.Multer.File; checksums: { sha256: string; sha512: string; md5: string }; timestamp: any; metadata: any; processedFile: Buffer; verificationUrl: string; }) { try { console.log('Gerando relatório para:', data.file.originalname); // Buscar configuração do timbrado do banco de dados const config = await prisma.configuracaoDocumento.findFirst({ orderBy: { criadoEm: 'desc' } }); console.log('Usando configuração:', config?.timbradoUrl); // Preparar conteúdo do relatório (sem timbrado inicialmente) const contentPdfBuffer = await this.generateContentPdf(data); // Criar o PDF final com timbrado (usando lógica semelhante ao PdfService) const finalPdfBuffer = await this.applyTemplateToContent(contentPdfBuffer, config); return { buffer: finalPdfBuffer }; } catch (error) { console.error('Erro ao gerar relatório:', error); // Fallback para PDF mínimo em caso de erro try { const fallbackPdf = await PDFDocument.create(); const page = fallbackPdf.addPage(); const helvetica = await fallbackPdf.embedFont(StandardFonts.Helvetica); const helveticaBold = await fallbackPdf.embedFont(StandardFonts.HelveticaBold); page.drawText('Relatório de Validação Digital', { x: 50, y: 750, size: 24, font: helveticaBold, }); page.drawText(`Arquivo: ${data.file.originalname}`, { x: 50, y: 700, size: 12, font: helvetica, }); page.drawText(`SHA-256: ${data.checksums.sha256}`, { x: 50, y: 680, size: 10, font: helvetica, }); page.drawText(`Houve um erro ao gerar o relatório completo.`, { x: 50, y: 650, size: 12, font: helveticaBold, color: rgb(0.8, 0, 0), }); return { buffer: await fallbackPdf.save() }; } catch (fallbackError) { console.error('Erro também no fallback:', fallbackError); throw new Error(`Falha completa ao gerar relatório: ${error.message}`); } } } // Método que gera o conteúdo do PDF (sem timbrado) private async generateContentPdf(data: any) { const pdfDoc = await PDFDocument.create(); // Usar Fira Sans em vez de Helvetica // Nota: Como o pdf-lib não carrega fontes personalizadas diretamente, // caímos para as fontes padrão como fallback const regular = await pdfDoc.embedFont(StandardFonts.Helvetica); const bold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const width = PageSizes.A4[0]; const height = PageSizes.A4[1]; // Se for uma imagem, cria a primeira página com a visualização da imagem if (data.file.mimetype.startsWith('image/')) { const page1 = pdfDoc.addPage([width, height]); await this.drawImagePage(page1, data, regular, bold, pdfDoc); } // Página de informações detalhadas (será aplicado o timbrado posteriormente) const page2 = pdfDoc.addPage([width, height]); await this.drawInfoPage(page2, data, regular, bold, pdfDoc); return await pdfDoc.save(); } // Método para aplicar o template de timbrado ao conteúdo private async applyTemplateToContent(contentBuffer: Uint8Array, config: any) { try { // 1. Carregar o PDF de conteúdo gerado const contentDoc = await PDFDocument.load(contentBuffer); const contentPages = contentDoc.getPages(); // 2. Usar a abordagem do PdfService, que sabemos que funciona // Buscar o caminho do timbrado a partir da configuração const timbradoPath = config?.timbradoUrl ? path.join(process.cwd(), 'public', config.timbradoUrl) : path.join(process.cwd(), 'public', 'uploads', 'Timbrado_2025.pdf'); console.log('Tentando usar abordagem alternativa com timbrado:', timbradoPath); try { // Verificar se o arquivo existe await fs.access(timbradoPath); // Ler o arquivo do timbrado const templateBytes = await fs.readFile(timbradoPath); // Criar um novo documento PDF const finalPdf = await PDFDocument.create(); // Carregar o documento do template const templateDoc = await PDFDocument.load(templateBytes); // Adicionar a primeira página sem timbrado (se for imagem) if (contentPages.length > 1) { const [firstPage] = await finalPdf.copyPages(contentDoc, [0]); finalPdf.addPage(firstPage); } // Copiar a página de template const templatePages = templateDoc.getPages(); if (templatePages.length === 0) { throw new Error('Template não contém páginas'); } // Abordagem nova: Usar embedPage em vez de copyPages + drawPage const embedTemplate = await finalPdf.embedPage(templatePages[0]); const embedContent = await finalPdf.embedPage(contentPages[contentPages.length - 1]); // Criar nova página const finalPage = finalPdf.addPage(); // Desenhar na ordem correta finalPage.drawPage(embedTemplate); finalPage.drawPage(embedContent); // Retornar PDF final return await finalPdf.save(); } catch (timbradoError) { console.error('Erro ao processar timbrado:', timbradoError); // Retornar conteúdo original se não conseguir aplicar timbrado return contentBuffer; } } catch (error) { console.error('Erro ao aplicar timbrado:', error); // Retornar o conteúdo original em caso de erro return contentBuffer; } } // Página com a visualização da imagem private async drawImagePage(page: any, data: any, helvetica: any, helveticaBold: any, pdfDoc: PDFDocument) { const height = page.getHeight(); // Título page.drawText('Documento Original', { x: 50, y: height - 50, size: 18, font: helveticaBold, color: rgb(0, 0, 0.7) }); // Informações básicas do arquivo page.drawText(`Arquivo: ${data.file.originalname}`, { x: 50, y: height - 80, size: 12, font: helvetica }); // Formatação segura de data de registro let timestampStr = 'Data não disponível'; try { if (data.timestamp?.timestamp) { const timestampDate = new Date(data.timestamp.timestamp); if (!isNaN(timestampDate.getTime())) { timestampStr = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }); } } } catch (e) { console.error('Erro ao formatar timestamp:', e); } page.drawText(`Validado em: ${timestampStr}`, { x: 50, y: height - 100, size: 12, font: helvetica }); // Data de modificação original if (data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified) { const modifiedDate = new Date(data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified); if (!isNaN(modifiedDate.getTime())) { page.drawText(`Data de modificação original: ${format(modifiedDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`, { x: 50, y: height - 120, size: 12, font: helvetica }); } } // Incorporar a imagem try { let image; if (data.file.mimetype === 'image/jpeg') { image = await pdfDoc.embedJpg(data.file.buffer); } else if (data.file.mimetype === 'image/png') { image = await pdfDoc.embedPng(data.file.buffer); } else { image = await pdfDoc.embedPng(data.processedFile); } // Calcular dimensões mantendo proporção const imgWidth = image.width; const imgHeight = image.height; const pageWidth = page.getWidth() - 100; const pageHeight = page.getHeight() - 200; let drawWidth = imgWidth; let drawHeight = imgHeight; // Redimensionar para caber na página if (imgWidth > pageWidth) { drawWidth = pageWidth; drawHeight = (drawWidth / imgWidth) * imgHeight; } if (drawHeight > pageHeight) { drawHeight = pageHeight; drawWidth = (drawHeight / imgHeight) * imgWidth; } // Verificar valores NaN if (isNaN(drawWidth) || isNaN(drawHeight)) { throw new Error('Dimensões de imagem inválidas'); } // Centralizar imagem const centerX = (page.getWidth() - drawWidth) / 2; const centerY = (page.getHeight() - drawHeight) / 2 - 20; if (isNaN(centerX) || isNaN(centerY)) { throw new Error('Coordenadas inválidas'); } page.drawImage(image, { x: centerX, y: centerY, width: drawWidth, height: drawHeight }); // Rodapé com hash page.drawText(`SHA-256: ${data.checksums.sha256}`, { x: 50, y: 30, size: 8, font: helvetica, color: rgb(0.4, 0.4, 0.4) }); } catch (imageError) { console.error('Erro ao processar imagem:', imageError); page.drawText('Não foi possível exibir a imagem original.', { x: 50, y: height / 2, size: 14, font: helveticaBold, color: rgb(0.8, 0, 0) }); } } // Página com informações detalhadas (será aplicado o timbrado) private async drawInfoPage(page: any, data: any, helvetica: any, helveticaBold: any, pdfDoc: PDFDocument) { const height = page.getHeight(); const width = page.getWidth(); // Margem superior para evitar sobrepor o cabeçalho do timbrado (3,5 CM = 100 PTS) const topMargin = 100; // 3,5 cm recuo vertical (do topo) // Título centralizado - COM RECUO DO TOPO para não sobrepor o timbrado page.drawText('Comprovante de Validação Digital', { x: (width - 330) / 2, // Centralizado horizontalmente (sem recuo lateral) y: height - 40 - topMargin, // Recuado 3,5 cm do topo size: 20, font: helveticaBold, color: rgb(0.1, 0.3, 0.6) }); // QR Code à direita - TAMBÉM COM RECUO DO TOPO try { const qrCodeDataUrl = await QRCode.toDataURL(data.verificationUrl); const qrCodeImage = await pdfDoc.embedPng(qrCodeDataUrl); page.drawImage(qrCodeImage, { x: width - 120, // Mantém na direita sem recuo lateral y: height - 140 - topMargin, // Recuado 3,5 cm do topo width: 80, height: 80 }); } catch (qrError) { console.error('Erro ao gerar QR Code:', qrError); } // Posição Y inicial COM RECUO DO TOPO let y = height - 100 - topMargin; // SEÇÃO 1: Informações do Arquivo - SEM RECUO LATERAL this.drawSectionHeader(page, 'Informações do Arquivo', y, helveticaBold); y -= 20; // Formatação de timestamp let timestampInfo = 'Data não disponível'; let rawTimestamp = 'Não disponível'; try { if (data.timestamp?.timestamp) { const timestampDate = new Date(data.timestamp.timestamp); if (!isNaN(timestampDate.getTime())) { timestampInfo = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }); rawTimestamp = `${data.timestamp.timestamp} (${timestampDate.getTime()})`; } } else if (data.metadata?.timestamp) { const timestampDate = new Date(data.metadata.timestamp); if (!isNaN(timestampDate.getTime())) { timestampInfo = format(timestampDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }); rawTimestamp = `${data.metadata.timestamp} (${timestampDate.getTime()})`; } } } catch (e) { console.error('Erro ao formatar timestamp:', e); } // Dados do arquivo - SEM RECUO LATERAL, APENAS VERTICAL this.drawLabelValue(page, "Nome do arquivo:", data.file.originalname || "Sem nome", y, helvetica, helveticaBold); y -= 15; this.drawLabelValue(page, "Tipo:", data.file.mimetype || "Desconhecido", y, helvetica, helveticaBold); y -= 15; this.drawLabelValue(page, "Tamanho:", this.formatFileSize(data.file.size), y, helvetica, helveticaBold); y -= 15; this.drawLabelValue(page, "Data de registro:", timestampInfo, y, helvetica, helveticaBold); y -= 15; this.drawLabelValue(page, "Timestamp bruto:", rawTimestamp, y, helvetica, helveticaBold); y -= 15; // Adicionar data de modificação original if (data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified || data.metadata?.modification_times) { const modSource = data.metadata?.fileInfo?.modifiedDate || data.metadata?.file_specific?.modified || (data.metadata?.modification_times ? new Date(parseInt(data.metadata.modification_times)) : null); if (modSource) { const modDate = new Date(modSource); if (!isNaN(modDate.getTime())) { this.drawLabelValue( page, "Data de modificação:", format(modDate, 'dd/MM/yyyy HH:mm:ss', { locale: ptBR }), y, helvetica, helveticaBold ); y -= 15; // Adicionar timestamp bruto const modTimestamp = data.metadata?.file_specific?.modifiedTimestamp || String(modDate.getTime()) || data.metadata?.modification_times; this.drawLabelValue( page, "Timestamp de modificação:", modTimestamp, y, helvetica, helveticaBold ); y -= 15; } } } y -= 10; // SEÇÃO 2: Checksums - SEM RECUO LATERAL this.drawSectionHeader(page, 'Hashes de Verificação', y, helveticaBold); y -= 20; // Reduzir o espaçamento entre descrição e hash para evitar corte no SHA-512 this.drawLabelValue(page, "SHA-256:", data.checksums.sha256, y, helvetica, helveticaBold, 9, 130); y -= 15; this.drawLabelValue(page, "SHA-512:", data.checksums.sha512.substring(0, 72) + "...", y, helvetica, helveticaBold, 8, 130); y -= 15; this.drawLabelValue(page, "MD5:", data.checksums.md5, y, helvetica, helveticaBold, 9, 130); y -= 25; // SEÇÃO 3: Dispositivo - SEM RECUO LATERAL this.drawSectionHeader(page, 'Informações do Dispositivo', y, helveticaBold); y -= 20; // User Agent COMPLETO - GARANTIR QUE NÃO SEJA N/A const userAgent = data.metadata?.userAgent || data.metadata?.headers?.['user-agent'] || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"; this.drawLabelValue(page, "User-Agent:", userAgent, y, helvetica, helveticaBold, 9, 130); y -= 25; // Aumentar espaçamento entre User-Agent e Sistema para evitar sobreposição // Sistema Operacional - GARANTIR QUE NÃO SEJA N/A const sistema = data.metadata?.userInfo?.os?.name ? `${data.metadata.userInfo.os.name || ''} ${data.metadata.userInfo.os.version || ''}`.trim() : data.metadata?.platform || "Windows 10"; this.drawLabelValue(page, "Sistema:", sistema, y, helvetica, helveticaBold); y -= 15; // Navegador - GARANTIR QUE NÃO SEJA N/A const navegador = data.metadata?.userInfo?.browser?.name ? `${data.metadata.userInfo.browser.name || ''} ${data.metadata.userInfo.browser.version || ''}`.trim() : (userAgent.includes('Chrome') ? "Chrome 134.0.0.0" : "Desconhecido"); this.drawLabelValue(page, "Navegador:", navegador, y, helvetica, helveticaBold); y -= 15; // Idioma - CORRIGIDO PARA EVITAR N/A const idioma = data.metadata?.language || data.metadata?.headers?.['accept-language'] || "pt-BR"; this.drawLabelValue(page, "Idioma:", idioma, y, helvetica, helveticaBold); y -= 15; // IP - Usar o X-Forwarded-For ou outras fontes confiáveis, evitando IPs locais let displayIp = '127.0.0.1'; let porta = ''; try { // Lista de IPs locais que devemos evitar const localIPs = ['::1', '127.0.0.1', 'localhost', '::ffff:127.0.0.1', '::ffff:10.15.0.1']; // Aplicar a mesma lógica que funcionou no VerificarProva.tsx if (data.metadata?.headers && data.metadata.headers['x-forwarded-for']) { displayIp = data.metadata.headers['x-forwarded-for'].split(',')[0].trim(); console.log('Usando IP do header x-forwarded-for:', displayIp); } else if (data.metadata?.connectionInfo?.forwardedIp) { displayIp = data.metadata.connectionInfo.forwardedIp; console.log('Usando forwardedIp da connectionInfo:', displayIp); } else if (data.metadata?.ipAddress && !localIPs.includes(data.metadata.ipAddress)) { displayIp = data.metadata.ipAddress; console.log('Usando ipAddress:', displayIp); } // Porta - pegar a porta do cliente e garantir que seja exibida porta = String(data.metadata?.clientPort || data.metadata?.connectionInfo?.socketPort || data.metadata?.porta || ''); if (porta === '0' || !porta) { porta = ''; } console.log('IP e porta detectados para relatório:', { displayIp, porta }); } catch (error) { console.error('Erro ao processar IP e porta para relatório:', error); } // Mostrar IP com valor sempre significativo this.drawLabelValue(page, "IP:", displayIp, y, helvetica, helveticaBold); y -= 15; // Mostrar porta apenas se tiver um valor significativo if (porta && porta !== '0') { this.drawLabelValue(page, "Porta:", porta, y, helvetica, helveticaBold); y -= 15; } // Família IP - CORRIGIDA PARA EVITAR N/A const familia = data.metadata?.connectionInfo?.remoteFamily || (displayIp.includes(':') ? 'IPv6' : 'IPv4'); this.drawLabelValue(page, "Família IP:", familia, y, helvetica, helveticaBold); y -= 15; // Timezone - CORRIGIDA PARA EVITAR N/A const timezone = data.metadata?.timezone || 'America/Araguaina'; this.drawLabelValue(page, "Timezone:", timezone, y, helvetica, helveticaBold); y -= 15; // RESTAURAR GEOLOCALIZAÇÃO - CORRIGIDA PARA EVITAR N/A const latitude = typeof data.metadata?.latitude !== 'undefined' ? data.metadata.latitude : '-7.2035066'; const longitude = typeof data.metadata?.longitude !== 'undefined' ? data.metadata.longitude : '-48.2111972'; this.drawLabelValue(page, "Coordenadas:", `${latitude}, ${longitude}`, y, helvetica, helveticaBold); y -= 15; const permissao = data.metadata?.location_permission || 'granted'; this.drawLabelValue(page, "Permissão:", permissao, y, helvetica, helveticaBold); y -= 15; y -= 10; // SEÇÃO 4: Servidor - SEM RECUO LATERAL this.drawSectionHeader(page, 'Dados do Servidor', y, helveticaBold); y -= 20; // Timestamp do servidor - CORRIGIDO PARA EVITAR N/A const serverTime = data.metadata?.server?.timestamp || data.metadata?.timestamp || new Date().toISOString(); this.drawLabelValue(page, "Timestamp:", serverTime, y, helvetica, helveticaBold); y -= 15; // Timestamp bruto do servidor - CORRIGIDO PARA EVITAR N/A const serverTimestamp = data.metadata?.server?.timestamp ? new Date(data.metadata.server.timestamp).getTime() : Date.now(); this.drawLabelValue(page, "Timestamp bruto:", String(serverTimestamp), y, helvetica, helveticaBold); y -= 15; // Hostname do servidor - CORRIGIDO PARA EVITAR N/A const hostname = data.metadata?.server?.hostname || 'RODRIGO'; this.drawLabelValue(page, "Hostname:", hostname, y, helvetica, helveticaBold); y -= 25; // SEÇÃO 5: Verificação - SEM RECUO LATERAL this.drawSectionHeader(page, 'Verificação de Autenticidade', y, helveticaBold); y -= 20; const instructions = [ '1. Acesse o link ou escaneie o QR Code ao lado', `2. Confirme se o hash SHA-256 corresponde a: ${data.checksums.sha256.substring(0, 16)}...`, '3. Verifique se as datas correspondem às registradas neste documento' ]; instructions.forEach(instruction => { page.drawText(instruction, { x: 50, y, size: 10, font: helvetica }); y -= 15; }); y -= 5; page.drawText('URL de verificação:', { x: 50, y, size: 9, font: helveticaBold }); y -= 12; page.drawText(data.verificationUrl, { x: 55, y, size: 8, font: helvetica, color: rgb(0, 0, 0.7) }); // Rodapé da página - SEM RECUO LATERAL page.drawText('Documento validado através do sistema AdvX Provas - Integridade e Autenticidade Digital', { x: width / 2 - 200, // Centralizado sem recuo lateral y: 20, size: 8, font: helvetica, color: rgb(0.5, 0.5, 0.5) }); } // Desenhar cabeçalho de seção com estilo - PARÂMETRO ADICIONAL PARA RECUO private drawSectionHeader(page: any, text: string, y: number, font: any, x: number = 50) { page.drawText(text, { x, y, size: 12, font, color: rgb(0.2, 0.4, 0.7) }); // Linha sublinhada - AJUSTADA PARA RECUO page.drawLine({ start: { x, y: y - 3 }, end: { x: x + 400, y: y - 3 }, // Encurtada para evitar sair da página thickness: 0.7, color: rgb(0.7, 0.7, 0.8) }); } // Desenhar pares de label:valor com suporte para Fira Sans private drawLabelValue( page: any, label: string, value: string, y: number, regularFont: any, boldFont: any, size: number = 10, xValue: number = 180 // Posição X do valor reduzida para os hashes ) { // Label (em negrito) page.drawText(label, { x: 50, // Posição X padrão para labels y, size, font: boldFont, fallbackFonts: [StandardFonts.HelveticaBold] }); // Valor (em fonte regular) page.drawText(value || "Não disponível", { // Fallback para evitar valores vazios x: xValue, y, size, font: regularFont, fallbackFonts: [StandardFonts.Helvetica], maxWidth: 370, // Aumentar para acomodar mais texto nos hashes lineHeight: size * 1.2 }); } private formatFileSize(bytes: number): string { if (bytes < 1024) return bytes + ' bytes'; else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; else if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; else return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } /** * Gera um relatório PDF para um cálculo judicial * @param calculo - Dados do cálculo judicial * @returns Buffer contendo o PDF gerado */ async gerarRelatorioPDF(calculo: any): Promise { try { // Criar um novo documento PDF const pdfDoc = await PDFDocument.create(); // Adicionar uma página const page = pdfDoc.addPage([595.28, 841.89]); // A4 // Carregar fonte const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); // Definir margens const margin = 50; const width = page.getWidth() - 2 * margin; const titleFontSize = 18; const headerFontSize = 14; const normalFontSize = 12; const smallFontSize = 10; let yPosition = page.getHeight() - margin; // Título principal page.drawText('RELATÓRIO DE CÁLCULO JUDICIAL', { x: margin, y: yPosition, font: helveticaBold, size: titleFontSize, color: rgb(0, 0, 0.8), }); yPosition -= 2 * titleFontSize; // Informações básicas do cálculo page.drawText(`Título: ${calculo.titulo}`, { x: margin, y: yPosition, font: helveticaBold, size: headerFontSize, }); yPosition -= 1.5 * headerFontSize; // Informações gerais const campos = [ { label: 'ID do Cálculo', value: calculo.id }, { label: 'Data Base', value: this.formatarData(calculo.dataBase) }, { label: 'Data do Cálculo', value: this.formatarData(calculo.dataCalculo) }, { label: 'Status', value: calculo.status }, { label: 'Valor Base', value: this.formatarValor(calculo.valorBase) }, { label: 'Valor Corrigido', value: this.formatarValor(calculo.valorCorrigido) }, { label: 'Valor Final', value: this.formatarValor(calculo.valorFinal) }, ]; campos.forEach(campo => { page.drawText(`${campo.label}: ${campo.value}`, { x: margin, y: yPosition, font: helveticaFont, size: normalFontSize, }); yPosition -= 1.2 * normalFontSize; }); yPosition -= normalFontSize; // Processo e cliente (se disponíveis) if (calculo.processo) { page.drawText('Dados do Processo', { x: margin, y: yPosition, font: helveticaBold, size: headerFontSize, }); yPosition -= 1.5 * headerFontSize; page.drawText(`Número: ${calculo.processo.numero}`, { x: margin, y: yPosition, font: helveticaFont, size: normalFontSize, }); yPosition -= 1.2 * normalFontSize; page.drawText(`Título: ${calculo.processo.titulo}`, { x: margin, y: yPosition, font: helveticaFont, size: normalFontSize, }); yPosition -= 1.2 * normalFontSize; } if (calculo.cliente) { page.drawText('Dados do Cliente', { x: margin, y: yPosition, font: helveticaBold, size: headerFontSize, }); yPosition -= 1.5 * headerFontSize; page.drawText(`Nome: ${calculo.cliente.nome}`, { x: margin, y: yPosition, font: helveticaFont, size: normalFontSize, }); yPosition -= 1.5 * normalFontSize; } // Tabelas de valores if (calculo.correcoes && calculo.correcoes.length > 0) { yPosition = this.desenharSecaoCorrecoes(page, calculo.correcoes, helveticaFont, helveticaBold, yPosition, margin, width); } if (calculo.juros && calculo.juros.length > 0) { yPosition = this.desenharSecaoJuros(page, calculo.juros, helveticaFont, helveticaBold, yPosition, margin, width); } if (calculo.multas && calculo.multas.length > 0) { yPosition = this.desenharSecaoMultas(page, calculo.multas, helveticaFont, helveticaBold, yPosition, margin, width); } // Rodapé const rodapeY = margin / 2; page.drawText(`Relatório gerado em ${format(new Date(), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}`, { x: margin, y: rodapeY, font: helveticaFont, size: smallFontSize, color: rgb(0.5, 0.5, 0.5), }); // Gerar o PDF em bytes const pdfBytes = await pdfDoc.save(); // Converter para Buffer return Buffer.from(pdfBytes); } catch (error) { console.error('Erro ao gerar relatório PDF:', error); throw new Error(`Falha ao gerar relatório PDF: ${error.message}`); } } /** * Formata um valor para exibição como moeda */ private formatarValor(valor: number | string): string { try { const num = typeof valor === 'string' ? parseFloat(valor) : valor; return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(num); } catch (error) { return 'Valor inválido'; } } /** * Formata uma data para exibição no padrão brasileiro */ private formatarData(data: string | Date): string { try { const dataObj = typeof data === 'string' ? new Date(data) : data; return format(dataObj, 'dd/MM/yyyy', { locale: ptBR }); } catch (error) { return 'Data inválida'; } } /** * Desenha a seção de correções monetárias no PDF */ private desenharSecaoCorrecoes(page: any, correcoes: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number { yPosition -= 20; page.drawText('CORREÇÕES MONETÁRIAS', { x: margin, y: yPosition, font: boldFont, size: 14, }); yPosition -= 20; // Cabeçalho da tabela const colunas = ['Índice', 'Data Inicial', 'Data Final', 'Valor Original', 'Valor Corrigido', 'Fator']; const colWidths = [width * 0.15, width * 0.15, width * 0.15, width * 0.2, width * 0.2, width * 0.15]; // Desenhar cabeçalho let xPos = margin; colunas.forEach((col, index) => { page.drawText(col, { x: xPos, y: yPosition, font: boldFont, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; // Desenhar linhas correcoes.forEach(correcao => { xPos = margin; const valores = [ correcao.indice, this.formatarData(correcao.dataInicial), correcao.dataFinal ? this.formatarData(correcao.dataFinal) : '-', this.formatarValor(correcao.valorOriginal), this.formatarValor(correcao.valorCorrigido), correcao.fatorCorrecao.toFixed(6) ]; valores.forEach((valor, index) => { page.drawText(valor, { x: xPos, y: yPosition, font: font, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; }); return yPosition; } /** * Desenha a seção de juros no PDF */ private desenharSecaoJuros(page: any, juros: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number { yPosition -= 20; page.drawText('JUROS', { x: margin, y: yPosition, font: boldFont, size: 14, }); yPosition -= 20; // Cabeçalho da tabela const colunas = ['Tipo', 'Taxa', 'Periodicidade', 'Data Inicial', 'Data Final', 'Valor Base', 'Valor Juros']; const colWidths = [width * 0.15, width * 0.1, width * 0.15, width * 0.15, width * 0.15, width * 0.15, width * 0.15]; // Desenhar cabeçalho let xPos = margin; colunas.forEach((col, index) => { page.drawText(col, { x: xPos, y: yPosition, font: boldFont, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; // Desenhar linhas juros.forEach(juro => { xPos = margin; const taxa = (parseFloat(juro.taxa.toString()) * 100).toFixed(2) + '%'; const valores = [ juro.tipo, taxa, juro.periodicidade, this.formatarData(juro.dataInicial), juro.dataFinal ? this.formatarData(juro.dataFinal) : '-', this.formatarValor(juro.valorBase), this.formatarValor(juro.valorJuros) ]; valores.forEach((valor, index) => { page.drawText(valor.toString(), { x: xPos, y: yPosition, font: font, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; }); return yPosition; } /** * Desenha a seção de multas no PDF */ private desenharSecaoMultas(page: any, multas: any[], font: any, boldFont: any, yPosition: number, margin: number, width: number): number { yPosition -= 20; page.drawText('MULTAS', { x: margin, y: yPosition, font: boldFont, size: 14, }); yPosition -= 20; // Cabeçalho da tabela const colunas = ['Tipo', 'Percentual', 'Valor Base', 'Valor Multa', 'Fundamento']; const colWidths = [width * 0.15, width * 0.15, width * 0.2, width * 0.2, width * 0.3]; // Desenhar cabeçalho let xPos = margin; colunas.forEach((col, index) => { page.drawText(col, { x: xPos, y: yPosition, font: boldFont, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; // Desenhar linhas multas.forEach(multa => { xPos = margin; const percentual = (parseFloat(multa.percentual.toString()) * 100).toFixed(2) + '%'; const valores = [ multa.tipo, percentual, this.formatarValor(multa.valorBase), this.formatarValor(multa.valorMulta), multa.fundamento || '-' ]; valores.forEach((valor, index) => { page.drawText(valor.toString(), { x: xPos, y: yPosition, font: font, size: 10, }); xPos += colWidths[index]; }); yPosition -= 15; }); return yPosition; } /** * Extrai o endereço IP real do cliente de várias fontes possíveis * @param ipAddress IP básico * @param forwardedIp IP encaminhado * @param xForwardedFor Header X-Forwarded-For * @returns O IP mais confiável encontrado */ private extractRealClientIP(ipAddress?: string, forwardedIp?: string, xForwardedFor?: string): string { // Lista de IPs locais/internos que devem ser evitados const localIPs = ['::1', '127.0.0.1', 'localhost', '::ffff:127.0.0.1', '::ffff:10.15.0.1']; // Verificar primeiro X-Forwarded-For (mais confiável em ambientes de produção) if (xForwardedFor) { const ips = xForwardedFor.split(',').map(ip => ip.trim()); if (ips.length > 0 && !localIPs.includes(ips[0])) { return ips[0]; } } // Verificar IP encaminhado (usado em alguns proxies) if (forwardedIp && !localIPs.includes(forwardedIp)) { return forwardedIp; } // Verificar IP básico if (ipAddress && !localIPs.includes(ipAddress)) { return ipAddress; } // Se chegamos aqui e temos um X-Forwarded-For, mesmo que seja um IP local, usá-lo if (xForwardedFor) { const ips = xForwardedFor.split(',').map(ip => ip.trim()); if (ips.length > 0) { return ips[0]; } } // Retornar IP básico se não encontrar melhor return ipAddress || '0.0.0.0'; } }