Files
advx/src/server/services/ReportGenerator.ts
T
2025-04-11 13:50:32 -03:00

1158 lines
39 KiB
TypeScript

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';
import sharp from 'sharp';
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 e informações básicas (mantidos como estão)
page.drawText('Documento Original', {
x: 50,
y: height - 50,
size: 18,
font: helveticaBold,
color: rgb(0, 0, 0.7)
});
page.drawText(`Arquivo: ${data.file.originalname}`, {
x: 50,
y: height - 80,
size: 12,
font: helvetica
});
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
});
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 com tratamento de erro mais robusto
try {
let image;
let imageBuffer = data.file.buffer;
// Se for JPEG, tentar pré-processar com sharp para garantir um formato válido
if (data.file.mimetype === 'image/jpeg') {
try {
// Converter para PNG primeiro - isso resolve muitos problemas de JPEGs corrompidos
const processedBuffer = await sharp(data.file.buffer)
.png() // Converter para PNG que é mais robusto
.toBuffer();
// Usar PNG em vez de JPEG para evitar o erro SOI
image = await pdfDoc.embedPng(processedBuffer);
console.log('JPEG convertido para PNG para evitar problemas de formatação');
} catch (sharpError) {
console.error('Erro ao processar JPEG com sharp:', sharpError);
// Se falhar, tentar abordagem alternativa
try {
// Tentar usar a versão processada se disponível
if (data.processedFile && data.processedFile.length > 0) {
image = await pdfDoc.embedPng(data.processedFile);
console.log('Usando versão processada da imagem');
} else {
throw new Error('Imagem processada não disponível');
}
} catch (fallbackError) {
console.error('Erro no fallback para imagem processada:', fallbackError);
throw new Error('Não foi possível processar a imagem JPEG');
}
}
}
// Para PNG ou outros formatos
else if (data.file.mimetype === 'image/png') {
image = await pdfDoc.embedPng(data.file.buffer);
}
// Para todos os outros tipos, tentar converter para PNG
else {
try {
const pngBuffer = await sharp(data.file.buffer)
.png()
.toBuffer();
image = await pdfDoc.embedPng(pngBuffer);
} catch (conversionError) {
console.error('Erro ao converter imagem para PNG:', conversionError);
throw new Error('Formato de imagem não suportado');
}
}
// 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);
// Mensagem de erro mais informativa na página
page.drawText('Não foi possível exibir a imagem original.', {
x: 50,
y: height / 2 + 20,
size: 14,
font: helveticaBold,
color: rgb(0.8, 0, 0)
});
page.drawText(`Erro: ${imageError.message}`, {
x: 50,
y: height / 2 - 10,
size: 12,
font: helvetica,
color: rgb(0.7, 0, 0)
});
page.drawText('O documento foi validado, mas a visualização não está disponível.', {
x: 50,
y: height / 2 - 30,
size: 12,
font: helvetica
});
}
}
// 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'];
// VERIFICAÇÃO DE DEBUG - mostrar os dados disponíveis no console
console.log('[PDF] Dados disponíveis para IP:', {
connectionInfo: data.metadata?.connectionInfo,
headers: data.metadata?.headers,
ipAddress: data.metadata?.ipAddress,
clientPort: data.metadata?.clientPort
});
// PRIORIDADE 1: headers['x-forwarded-for'] - Priorizar PRIMEIRO este header
if (data.metadata?.headers && data.metadata.headers['x-forwarded-for']) {
const forwardedIps = String(data.metadata.headers['x-forwarded-for']).split(',');
if (forwardedIps.length > 0) {
displayIp = forwardedIps[0].trim();
console.log('[PDF] Usando IP do x-forwarded-for:', displayIp);
}
}
// PRIORIDADE 2: connectionInfo.forwardedIp
else if (data.metadata?.connectionInfo?.forwardedIp) {
displayIp = data.metadata.connectionInfo.forwardedIp;
console.log('[PDF] Usando forwardedIp da connectionInfo:', displayIp);
}
// PRIORIDADE 3: ipAddress
else if (data.metadata?.ipAddress && !localIPs.includes(data.metadata.ipAddress)) {
displayIp = data.metadata.ipAddress;
console.log('[PDF] Usando ipAddress direto:', displayIp);
}
// Porta - pegar a porta do cliente de várias fontes possíveis
if (data.metadata?.connectionInfo?.socketPort) {
porta = String(data.metadata.connectionInfo.socketPort);
console.log('[PDF] Usando porta da connectionInfo:', porta);
}
else if (data.metadata?.clientPort) {
porta = String(data.metadata.clientPort);
console.log('[PDF] Usando clientPort:', porta);
}
else if (data.metadata?.porta) {
porta = String(data.metadata.porta);
console.log('[PDF] Usando porta do objeto metadata:', porta);
}
// GARANTIR que não estamos usando 127.0.0.1
if (displayIp === '127.0.0.1' || localIPs.includes(displayIp)) {
// Tentar novamente com as informações de conexão
if (data.metadata?.connectionInfo?.forwardedIp) {
displayIp = data.metadata.connectionInfo.forwardedIp;
console.log('[PDF] Forçando uso do forwardedIp:', displayIp);
}
}
console.log('[PDF] IP e porta finais para relatório:', { displayIp, porta });
} catch (error) {
console.error('[PDF] Erro ao processar IP e porta para relatório:', error);
}
// Mostrar IP com valor sempre significativo - NUNCA usar 127.0.0.1
if (displayIp === '127.0.0.1' || displayIp === '0.0.0.0') {
// Último recurso - pegar direto do objeto connectionInfo.forwardedIp
displayIp = data.metadata?.connectionInfo?.forwardedIp ||
(data.metadata?.headers ? data.metadata.headers['x-forwarded-for'] : '0.0.0.0');
console.log('[PDF] IP final (após verificação de IP local):', displayIp);
}
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<Buffer> {
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';
}
}