first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
import OpenTimestamps from 'opentimestamps-javascript';
import { createHash } from 'crypto';
import * as fs from 'fs/promises';
import * as path from 'path';
export class BlockchainService {
private readonly storageDir: string;
constructor() {
this.storageDir = path.join(process.cwd(), 'storage', 'timestamps');
this.ensureStorageDir();
}
private async ensureStorageDir() {
try {
await fs.mkdir(this.storageDir, { recursive: true });
} catch (error) {
console.error('Erro ao criar diretório de timestamps:', error);
}
}
async createTimestamp(buffer: Buffer) {
try {
console.log('Iniciando criação de timestamp na blockchain...');
// Criar hash do arquivo e usar diretamente
const fileHash = createHash('sha256').update(buffer).digest('hex');
console.log('Hash gerado:', fileHash);
// Criar timestamp usando o hash em hex
const timestamp = await OpenTimestamps.stamp(fileHash);
if (!timestamp) {
throw new Error('Falha ao gerar timestamp');
}
console.log('Timestamp criado com sucesso');
// Salvar arquivo .ots
const otsPath = path.join(this.storageDir, `${fileHash}.ots`);
await fs.writeFile(otsPath, Buffer.from(timestamp));
console.log('Arquivo .ots salvo em:', otsPath);
return {
hash: fileHash,
timestamp,
otsPath
};
} catch (error) {
console.error('Erro detalhado ao criar timestamp:', error);
console.error('Stack:', error.stack);
throw new Error(`Falha ao criar timestamp na blockchain: ${error.message}`);
}
}
async verifyTimestamp(hash: string, timestamp: Buffer) {
try {
const result = await OpenTimestamps.verify(
Buffer.from(timestamp),
Buffer.from(hash, 'hex')
);
if (!result) {
return {
verified: false,
message: 'Timestamp ainda não confirmado na blockchain'
};
}
return {
verified: true,
timestamp: new Date(result * 1000),
message: 'Timestamp verificado na blockchain do Bitcoin'
};
} catch (error) {
console.error('Erro ao verificar timestamp:', error);
return {
verified: false,
message: 'Erro ao verificar timestamp'
};
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,45 @@
import { createHash } from 'crypto';
/**
* Versão simplificada do MetadataCollector para quando faltam dependências
*/
export class FallbackMetadataCollector {
/**
* Coleta metadados básicos a partir dos dados fornecidos
*/
async collectMetadata(data: {
userAgent: string;
ipAddress: string;
timestamp: Date;
fileName: string;
fileSize: number;
[key: string]: any;
}) {
const timestamp = new Date();
// Retornar metadados simplificados
return {
timestamp: timestamp.toISOString(),
collectionTime: timestamp.toISOString(),
userAgent: data.userAgent,
ipAddress: data.ipAddress,
fileName: data.fileName,
fileSize: data.fileSize,
fileSizeFormatted: this.formatFileSize(data.fileSize),
integrityHash: this.generateHash(data.fileName, data.fileSize.toString(), timestamp.toISOString())
};
}
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';
}
private generateHash(...values: string[]): string {
return createHash('sha256')
.update(values.join('|'))
.digest('hex');
}
}
+63
View File
@@ -0,0 +1,63 @@
import { createHash } from 'crypto';
export class FileValidator {
async validateFile(buffer: Buffer, mimeType: string) {
try {
// Validação básica
if (!buffer || buffer.length === 0) {
return { valid: false, error: 'Arquivo vazio' };
}
// Verificar tamanho máximo (10MB)
const MAX_SIZE = 10 * 1024 * 1024;
if (buffer.length > MAX_SIZE) {
return { valid: false, error: 'Arquivo muito grande (máximo 10MB)' };
}
// Verificar tipos de arquivo permitidos
const allowedTypes = [
'application/pdf',
'image/jpeg',
'image/png', // Corrigido o tipo MIME para PNG
'image/gif',
'image.webp'
];
if (!allowedTypes.includes(mimeType)) {
return { valid: false, error: 'Tipo de arquivo não suportado. Use PDF, JPEG, PNG ou GIF.' };
}
return { valid: true };
} catch (error) {
console.error('Erro na validação de arquivo:', error);
return { valid: false, error: `Erro ao validar arquivo: ${error.message}` };
}
}
async validateMetadata(metadata: any) {
// Campos básicos obrigatórios
const requiredFields = ['userAgent', 'timestamp'];
const missingFields = requiredFields.filter(field => !metadata[field]);
if (missingFields.length > 0) {
return {
valid: false,
error: `Dados necessários ausentes: ${missingFields.join(', ')}`
};
}
return { valid: true };
}
generateHashes(buffer: Buffer) {
return {
sha256: createHash('sha256').update(buffer).digest('hex'),
sha512: createHash('sha512').update(buffer).digest('hex'),
md5: createHash('md5').update(buffer).digest('hex')
};
}
async cleanup() {
// Nada a fazer nesta implementação simplificada
}
}
@@ -0,0 +1,931 @@
import { PrismaClient, Decimal } from '@prisma/client';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid'; // Importar uuid para gerar IDs
import { prisma } from '../prismaClient';
import { Decimal } from '@prisma/client/runtime/library';
const prisma = new PrismaClient();
export class IndiceMonetarioService {
/**
* Adiciona um novo índice monetário
*/
async adicionarIndice(data: {
nome: string;
mesReferencia: number;
anoReferencia: number;
valor: number;
dataPublicacao?: Date;
fonte?: string;
}) {
try {
// Verificar se já existe
const existente = await prisma.indiceMonetario.findFirst({
where: {
nome: data.nome,
mesReferencia: data.mesReferencia,
anoReferencia: data.anoReferencia,
}
});
if (existente) {
// Atualizar o existente
return await prisma.indiceMonetario.update({
where: { id: existente.id },
data: {
valor: new Decimal(data.valor),
dataPublicacao: data.dataPublicacao,
fonte: data.fonte,
atualizadoEm: new Date()
}
});
}
// Criar novo
return await prisma.indiceMonetario.create({
data: {
id: uuidv4(), // Gerar ID UUID explicitamente
nome: data.nome,
mesReferencia: data.mesReferencia,
anoReferencia: data.anoReferencia,
valor: new Decimal(data.valor),
dataPublicacao: data.dataPublicacao,
fonte: data.fonte
}
});
} catch (error) {
console.error('Erro ao adicionar índice monetário:', error);
throw new Error(`Falha ao adicionar índice: ${error.message}`);
}
}
/**
* Lista índices monetários com filtros
*/
async listarIndices(filtros?: {
nome?: string;
anoReferencia?: number;
fonte?: string;
}) {
try {
const where: any = {};
if (filtros) {
if (filtros.nome) {
where.nome = filtros.nome;
}
if (filtros.anoReferencia) {
where.anoReferencia = filtros.anoReferencia;
}
if (filtros.fonte) {
where.fonte = { contains: filtros.fonte };
}
}
return await prisma.indiceMonetario.findMany({
where,
orderBy: [
{ anoReferencia: 'desc' },
{ mesReferencia: 'desc' },
{ nome: 'asc' }
]
});
} catch (error) {
console.error('Erro ao listar índices monetários:', error);
throw new Error(`Falha ao listar índices: ${error.message}`);
}
}
/**
* Busca um índice específico
*/
async buscarIndice(data: {
nome: string;
mesReferencia: number;
anoReferencia: number;
}) {
try {
return await prisma.indiceMonetario.findFirst({
where: {
nome: data.nome,
mesReferencia: data.mesReferencia,
anoReferencia: data.anoReferencia
}
});
} catch (error) {
console.error('Erro ao buscar índice monetário:', error);
throw new Error(`Falha ao buscar índice: ${error.message}`);
}
}
/**
* Remove um índice monetário
*/
async removerIndice(id: string) {
try {
return await prisma.indiceMonetario.delete({
where: { id }
});
} catch (error) {
console.error('Erro ao remover índice monetário:', error);
throw new Error(`Falha ao remover índice: ${error.message}`);
}
}
/**
* Importa índices INPC do IBGE
*/
async importarIndicesINPC() {
try {
console.log('Iniciando importação de índices INPC...');
// Buscar últimos 120 meses
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
const response = await axios.get(url);
const data = response.data[0];
if (!data?.resultados?.[0]?.series?.[0]?.serie) {
throw new Error('Formato de resposta inválido do IBGE');
}
const indices = data.resultados[0].series[0].serie;
// Processar e salvar os índices
for (const [periodo, valor] of Object.entries(indices)) {
const ano = parseInt(periodo.substring(0, 4));
const mes = parseInt(periodo.substring(4));
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
await prisma.indiceMonetario.upsert({
where: {
nome_mesReferencia_anoReferencia: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano
}
},
update: {
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
},
create: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
}
});
console.log(`Índice importado: ${mes}/${ano} = ${valorNumerico}%`);
}
}
console.log('Importação concluída com sucesso');
} catch (error) {
console.error('Erro ao importar índices:', error);
throw new Error('Falha ao importar índices do IBGE');
}
}
/**
* Importa índices SELIC do Banco Central
*/
async importarIndicesSELIC() {
try {
// Usar a API do Banco Central em vez de CSV
const hoje = new Date();
const dataFim = `${hoje.getFullYear()}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`;
// Pegar dados do último ano
const dataInicio = `${hoje.getFullYear() - 1}-${String(hoje.getMonth() + 1).padStart(2, '0')}-${String(hoje.getDate()).padStart(2, '0')}`;
const url = `https://api.bcb.gov.br/dados/serie/bcdata.sgs.11/dados?formato=json&dataInicial=${dataInicio}&dataFinal=${dataFim}`;
const response = await axios.get(url);
if (!Array.isArray(response.data)) {
throw new Error('Formato de resposta da API do Banco Central inválido');
}
const indices = [];
for (const item of response.data) {
// Tratar a data (formato DD/MM/YYYY)
const dataParts = item.data.split('/');
const ano = parseInt(dataParts[2]);
const mes = parseInt(dataParts[1]);
const valorNumerico = parseFloat(item.valor);
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
await this.adicionarIndice({
nome: 'SELIC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico / 100, // Converter para decimal (ex: 13.25% vira 0.1325)
dataPublicacao: new Date(),
fonte: 'BACEN'
});
indices.push({
nome: 'SELIC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico / 100
});
}
}
return {
success: true,
message: `Importados ${indices.length} índices SELIC`,
indices
};
} catch (error) {
console.error('Erro ao importar índices SELIC:', error);
throw new Error(`Falha ao importar índices SELIC: ${error.message}`);
}
}
/**
* Calcula o fator de correção entre duas datas para um índice
*/
async calcularFatorCorrecao(data: {
indice: string;
dataInicial: Date;
dataFinal: Date;
}) {
try {
const { indice, dataInicial, dataFinal } = data;
// Obter o mês/ano das datas
const mesInicial = dataInicial.getMonth() + 1; // getMonth retorna 0-11
const anoInicial = dataInicial.getFullYear();
const mesFinal = dataFinal.getMonth() + 1;
const anoFinal = dataFinal.getFullYear();
// Buscar todos os índices necessários para o período
const indices = await prisma.indiceMonetario.findMany({
where: {
nome: indice,
OR: [
// Índices do ano inicial
{
anoReferencia: anoInicial,
mesReferencia: { gte: mesInicial }
},
// Anos intermediários (se houver)
{
anoReferencia: { gt: anoInicial, lt: anoFinal }
},
// Índices do ano final
{
anoReferencia: anoFinal,
mesReferencia: { lte: mesFinal }
}
]
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' }
]
});
// Se não houver índices suficientes, usar uma estimativa
if (indices.length === 0) {
const mesesDiferenca =
(anoFinal - anoInicial) * 12 + (mesFinal - mesInicial);
// Taxa média mensal estimada (usar 0.5% ao mês como estimativa conservadora)
const taxaMediaMensal = 0.005;
return new Decimal(Math.pow(1 + taxaMediaMensal, mesesDiferenca));
}
// Calcular o fator de correção acumulado
let fatorAcumulado = new Decimal(1);
for (const indice of indices) {
// Somar 1 ao valor do índice (ex: índice de 0.0082 vira fator de 1.0082)
const fator = new Decimal(1).add(indice.valor);
fatorAcumulado = fatorAcumulado.mul(fator);
}
return fatorAcumulado;
} catch (error) {
console.error('Erro ao calcular fator de correção:', error);
throw new Error(`Falha ao calcular fator de correção: ${error.message}`);
}
}
}
// Função para baixar índices do IBGE
export async function atualizarIndicesINPC() {
try {
console.log('Iniciando atualização dos índices INPC...');
// Definir período de busca (5 anos para trás)
const hoje = new Date();
const anoInicial = hoje.getFullYear() - 5;
// Buscar último índice no banco
const ultimoIndice = await prisma.indiceMonetario.findFirst({
orderBy: [
{ anoReferencia: 'desc' },
{ mesReferencia: 'desc' }
]
});
// URL da API do IBGE para INPC (últimos 120 meses = 10 anos)
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
console.log('Buscando índices desde:', anoInicial);
const response = await axios.get(url);
if (!response.data?.[0]?.resultados?.[0]?.series?.[0]?.serie) {
throw new Error('Formato de resposta inválido do IBGE');
}
const indices = response.data[0].resultados[0].series[0].serie;
let totalIndices = 0;
// Processar cada índice
for (const [periodo, valor] of Object.entries(indices)) {
const ano = parseInt(periodo.substring(0, 4));
const mes = parseInt(periodo.substring(4));
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
// Verificar se é um índice válido
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
await prisma.indiceMonetario.upsert({
where: {
nome_mesReferencia_anoReferencia: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano
}
},
update: {
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
},
create: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
}
});
totalIndices++;
console.log(`Índice atualizado: ${mes}/${ano} = ${valorNumerico}%`);
}
}
console.log(`Atualização concluída. Total de índices: ${totalIndices}`);
return true;
} catch (error) {
console.error('Erro ao atualizar índices INPC:', error);
throw error;
}
}
// Função para verificar se os índices existem para um período
export async function verificarIndicesDisponiveis(dataInicial: Date, dataFinal: Date): Promise<boolean> {
try {
const indices = await prisma.indiceMonetario.findFirst({
where: {
nome: 'INPC',
AND: [
{
anoReferencia: dataInicial.getFullYear(),
mesReferencia: dataInicial.getMonth() + 1
}
]
}
});
// Se não encontrar índices, tenta atualizar
if (!indices) {
console.log('Índices não encontrados. Iniciando atualização...');
await atualizarIndicesINPC();
return true;
}
return true;
} catch (error) {
console.error('Erro ao verificar índices:', error);
return false;
}
}
export async function getIndicesParaPeriodo({
indice,
dataInicial,
dataFinal
}: {
indice: string;
dataInicial: Date;
dataFinal: Date;
}) {
try {
// Buscar índices no período
let indices = await prisma.indiceMonetario.findMany({
where: {
nome: indice,
AND: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear()
}
},
{
OR: [
// Índices do primeiro ano
{
anoReferencia: dataInicial.getFullYear(),
mesReferencia: {
gte: dataInicial.getMonth() + 1
}
},
// Índices do último ano
{
anoReferencia: dataFinal.getFullYear(),
mesReferencia: {
lte: dataFinal.getMonth() + 1
}
},
// Índices dos anos intermediários
{
anoReferencia: {
gt: dataInicial.getFullYear(),
lt: dataFinal.getFullYear()
}
}
]
}
]
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' }
]
});
// Se não encontrar índices, tentar importar
if (!indices || indices.length === 0) {
console.log('Índices não encontrados. Iniciando importação...');
await importarIndicesINPC();
// Buscar novamente após importação
indices = await prisma.indiceMonetario.findMany({
where: {
nome: indice,
AND: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear()
}
},
{
OR: [
// Índices do primeiro ano
{
anoReferencia: dataInicial.getFullYear(),
mesReferencia: {
gte: dataInicial.getMonth() + 1
}
},
// Índices do último ano
{
anoReferencia: dataFinal.getFullYear(),
mesReferencia: {
lte: dataFinal.getMonth() + 1
}
},
// Índices dos anos intermediários
{
anoReferencia: {
gt: dataInicial.getFullYear(),
lt: dataFinal.getFullYear()
}
}
]
}
]
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' }
]
});
}
if (!indices.length) {
throw new Error('Não foram encontrados índices para o período');
}
return indices;
} catch (error) {
console.error('Erro ao buscar/importar índices:', error);
throw error;
}
}
export async function importarIndicesINPC() {
try {
// API do IBGE para INPC
const url = 'https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-120/variaveis/44?localidades=N1[all]';
console.log('Buscando índices do IBGE...');
const response = await axios.get(url);
if (!response.data || !response.data[0] || !response.data[0].resultados) {
throw new Error('Formato de resposta inválido da API do IBGE');
}
const dados = response.data[0].resultados[0].series[0].serie;
// Processar e salvar os índices
for (const [periodo, valor] of Object.entries(dados)) {
const ano = parseInt(periodo.substring(0, 4));
const mes = parseInt(periodo.substring(4));
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
await prisma.indiceMonetario.upsert({
where: {
nome_mesReferencia_anoReferencia: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano
}
},
update: {
valor: valorNumerico / 100 + 1, // Converter para fator (ex: 1.0053 para 0.53%)
dataPublicacao: new Date(),
fonte: 'IBGE'
},
create: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico / 100 + 1,
dataPublicacao: new Date(),
fonte: 'IBGE'
}
});
}
}
console.log('Índices INPC importados com sucesso');
return true;
} catch (error) {
console.error('Erro ao importar índices INPC:', error);
throw error;
}
}
async function obterIndicesNecessarios(dataInicial: Date, dataFinal: Date) {
try {
// 1. Verificar índices existentes no banco
const indicesExistentes = await prisma.indiceMonetario.findMany({
where: {
nome: 'INPC',
AND: [
{
OR: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear(),
},
},
],
},
],
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' },
],
});
// 2. Criar mapa dos índices existentes para fácil verificação
const mapaIndices = new Map();
indicesExistentes.forEach(indice => {
const chave = `${indice.anoReferencia}-${indice.mesReferencia.toString().padStart(2, '0')}`;
mapaIndices.set(chave, indice);
});
// 3. Verificar quais meses estão faltando
const mesesFaltantes: { ano: number; mes: number }[] = [];
let dataAtual = new Date(dataInicial);
while (dataAtual <= dataFinal) {
const chave = `${dataAtual.getFullYear()}-${(dataAtual.getMonth() + 1).toString().padStart(2, '0')}`;
if (!mapaIndices.has(chave)) {
mesesFaltantes.push({
ano: dataAtual.getFullYear(),
mes: dataAtual.getMonth() + 1
});
}
// Avançar para o próximo mês
dataAtual.setMonth(dataAtual.getMonth() + 1);
}
// 4. Se houver meses faltantes, buscar da API
if (mesesFaltantes.length > 0) {
console.log(`Buscando ${mesesFaltantes.length} índices faltantes do IBGE...`);
// Calcular o número de meses entre a data mais antiga e mais recente necessária
const mesesTotal = Math.ceil(
(dataFinal.getTime() - dataInicial.getTime()) / (1000 * 60 * 60 * 24 * 30.44)
);
// Buscar um período maior que o necessário para garantir
const periodoAPI = Math.max(mesesTotal + 12, 120); // Mínimo de 120 meses (10 anos)
const url = `https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/-${periodoAPI}/variaveis/44?localidades=N1[all]`;
const response = await axios.get(url);
const data = response.data[0];
if (!data?.resultados?.[0]?.series?.[0]?.serie) {
throw new Error('Formato de resposta inválido do IBGE');
}
const indices = data.resultados[0].series[0].serie;
// Criar os índices faltantes
for (const [periodo, valor] of Object.entries(indices)) {
const ano = parseInt(periodo.substring(0, 4));
const mes = parseInt(periodo.substring(4));
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
// Verificar se este índice é necessário
const chave = `${ano}-${mes.toString().padStart(2, '0')}`;
if (!mapaIndices.has(chave) &&
ano >= dataInicial.getFullYear() &&
ano <= dataFinal.getFullYear()) {
await prisma.indiceMonetario.upsert({
where: {
nome_mesReferencia_anoReferencia: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano
}
},
update: {
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
},
create: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
}
});
console.log(`Índice criado: ${mes}/${ano} = ${valorNumerico}%`);
}
}
}
}
// 5. Retornar todos os índices necessários
return await prisma.indiceMonetario.findMany({
where: {
nome: 'INPC',
AND: [
{
OR: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear(),
},
},
],
},
],
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' },
],
});
} catch (error) {
console.error('Erro ao obter índices:', error);
throw error;
}
}
// Atualizar a função principal para usar a nova função de obtenção de índices
export async function calcularFatorCorrecaoTotal({
dataInicial,
dataFinal,
indice
}: {
dataInicial: Date;
dataFinal: Date;
indice: string;
}): Promise<Decimal> {
try {
// Buscar ou criar todos os índices necessários
const indices = await obterIndicesNecessarios(dataInicial, dataFinal);
if (!indices.length) {
throw new Error('Não foram encontrados índices para o período mesmo após tentativa de atualização');
}
// Calcular fator acumulado
let fatorAcumulado = new Decimal(1);
for (const indice of indices) {
fatorAcumulado = fatorAcumulado.mul(new Decimal(1 + indice.valor / 100));
}
console.log('Fator de correção calculado:', {
periodo: `${dataInicial.toISOString()} até ${dataFinal.toISOString()}`,
indices: indices.length,
primeiro: indices[0],
ultimo: indices[indices.length - 1],
fatorAcumulado: fatorAcumulado.toFixed(6)
});
return fatorAcumulado;
} catch (error) {
console.error('Erro ao calcular fator de correção:', error);
throw error;
}
}
export async function buscarIndicesParaPeriodo(dataInicial: Date, dataFinal: Date) {
console.log(`Buscando índices para o período de ${dataInicial.toISOString()} até ${dataFinal.toISOString()}`);
try {
// Primeiro verificar quais meses já temos
const indicesExistentes = await prisma.indiceMonetario.findMany({
where: {
nome: 'INPC',
OR: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear()
}
}
]
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' }
]
});
console.log(`Encontrados ${indicesExistentes.length} índices no banco`);
// Se não houver índices suficientes, buscar da API do IBGE
if (indicesExistentes.length === 0) {
console.log('Buscando índices do IBGE...');
// Configurar timeout maior para a requisição
const axiosInstance = axios.create({
timeout: 30000 // 30 segundos
});
// Fazer múltiplas requisições em paralelo para diferentes períodos
const promises = [];
let anoAtual = dataInicial.getFullYear();
const anoFinal = dataFinal.getFullYear();
while (anoAtual <= anoFinal) {
const url = `https://servicodados.ibge.gov.br/api/v3/agregados/1736/periodos/${anoAtual}/variaveis/44?localidades=N1[all]`;
promises.push(axiosInstance.get(url));
anoAtual++;
}
console.log(`Fazendo ${promises.length} requisições ao IBGE...`);
const responses = await Promise.allSettled(promises);
// Processar cada resposta
for (const response of responses) {
if (response.status === 'fulfilled' && response.value.data?.[0]?.resultados?.[0]?.series?.[0]?.serie) {
const indices = response.value.data[0].resultados[0].series[0].serie;
for (const [periodo, valor] of Object.entries(indices)) {
const ano = parseInt(periodo.substring(0, 4));
const mes = parseInt(periodo.substring(4));
const valorNumerico = parseFloat(String(valor).replace(',', '.'));
if (!isNaN(ano) && !isNaN(mes) && !isNaN(valorNumerico)) {
await prisma.indiceMonetario.upsert({
where: {
nome_mesReferencia_anoReferencia: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano
}
},
update: {
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
},
create: {
nome: 'INPC',
mesReferencia: mes,
anoReferencia: ano,
valor: valorNumerico,
dataPublicacao: new Date(),
fonte: 'IBGE'
}
});
console.log(`Índice salvo: ${mes}/${ano} = ${valorNumerico}%`);
}
}
}
}
// Buscar novamente após importação
return await prisma.indiceMonetario.findMany({
where: {
nome: 'INPC',
OR: [
{
anoReferencia: {
gte: dataInicial.getFullYear(),
lte: dataFinal.getFullYear()
}
}
]
},
orderBy: [
{ anoReferencia: 'asc' },
{ mesReferencia: 'asc' }
]
});
}
return indicesExistentes;
} catch (error) {
console.error('Erro ao buscar índices:', error);
throw new Error(`Falha ao buscar índices monetários: ${error.message}`);
}
}
// Atualizar a função principal para usar o novo método
export async function calcularFatorCorrecaoTotal({
dataInicial,
dataFinal,
indice
}: {
dataInicial: Date;
dataFinal: Date;
indice: string;
}): Promise<Decimal> {
try {
const indices = await buscarIndicesParaPeriodo(dataInicial, dataFinal);
if (!indices.length) {
throw new Error('Não foram encontrados índices para o período informado');
}
// Calcular fator acumulado
let fatorAcumulado = new Decimal(1);
for (const indice of indices) {
fatorAcumulado = fatorAcumulado.mul(new Decimal(1 + indice.valor / 100));
}
console.log('Fator de correção calculado:', {
indices: indices.length,
primeiro: indices[0],
ultimo: indices[indices.length - 1],
fatorAcumulado: fatorAcumulado.toString()
});
return fatorAcumulado;
} catch (error) {
console.error('Erro ao calcular fator de correção:', error);
throw error;
}
}
+246
View File
@@ -0,0 +1,246 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { createHash } from 'crypto';
// Tentativa de importar o UAParser com fallback para evitar quebra caso não esteja instalado
let UAParser;
try {
UAParser = require('ua-parser-js');
} catch (error) {
console.warn('Aviso: UAParser não está disponível. Usando método alternativo para analisar User-Agent.');
// Implementação simples de fallback para quando a biblioteca não está disponível
UAParser = class SimpleBrowserDetect {
constructor(userAgent) {
this.userAgent = userAgent;
}
getResult() {
const isMobile = /mobile|android|iphone|ipod|ipad/i.test(this.userAgent);
const isWindows = /windows/i.test(this.userAgent);
const isMac = /macintosh|mac os/i.test(this.userAgent);
const isLinux = /linux/i.test(this.userAgent);
const chromeMatch = this.userAgent.match(/chrome\/(\d+)/i);
const firefoxMatch = this.userAgent.match(/firefox\/(\d+)/i);
const safariMatch = this.userAgent.match(/safari\/(\d+)/i);
const edgeMatch = this.userAgent.match(/edge\/(\d+)|edg\/(\d+)/i);
return {
browser: {
name: edgeMatch ? 'Edge' :
chromeMatch ? 'Chrome' :
firefoxMatch ? 'Firefox' :
safariMatch ? 'Safari' : 'Desconhecido',
version: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
major: (edgeMatch || chromeMatch || firefoxMatch || safariMatch || ['', '0'])[1],
},
engine: {
name: 'Desconhecido',
version: '',
},
os: {
name: isWindows ? 'Windows' :
isMac ? 'Mac OS' :
isLinux ? 'Linux' : 'Desconhecido',
version: '',
},
device: {
vendor: '',
model: '',
type: isMobile ? 'mobile' : 'desktop',
},
cpu: {
architecture: '',
}
};
}
};
}
/**
* Classe para coletar e validar metadados de arquivos conforme RFC 3227
* (Diretrizes para Coleta e Arquivamento de Evidências)
*/
export class MetadataCollector {
/**
* Coleta metadados avançados a partir dos dados fornecidos
*/
async collectMetadata(data: {
userAgent: string;
ipAddress: string;
timestamp: Date;
fileName: string;
fileModified?: Date;
fileSize: number;
viewportWidth?: string;
viewportHeight?: string;
language?: string;
referrer?: string;
latitude?: number;
longitude?: number;
}) {
try {
// Gerar identificador único para a coleta
const collectionId = uuidv4();
const collectionTimestamp = new Date();
// Parser para informações do navegador
const parser = new UAParser(data.userAgent);
const browserInfo = parser.getResult();
// Obter geolocalização a partir do IP se não fornecida diretamente
let geoInfo = null;
if (!data.latitude && !data.longitude) {
try {
geoInfo = await this.getGeolocationFromIP(data.ipAddress);
} catch (error) {
console.warn('Falha ao obter geolocalização:', error);
}
}
// Construir o objeto de metadados
return {
// Identificação da coleta
collectionId,
collectionTimestamp: collectionTimestamp.toISOString(),
// Dados do usuário
userInfo: {
ipAddress: data.ipAddress,
userAgent: data.userAgent,
language: data.language || 'não informado',
browser: {
name: browserInfo.browser.name || 'desconhecido',
version: browserInfo.browser.version || 'desconhecido',
major: browserInfo.browser.major || 'desconhecido',
},
engine: {
name: browserInfo.engine.name || 'desconhecido',
version: browserInfo.engine.version || 'desconhecido',
},
os: {
name: browserInfo.os.name || 'desconhecido',
version: browserInfo.os.version || 'desconhecido',
},
device: {
vendor: browserInfo.device.vendor || 'desconhecido',
model: browserInfo.device.model || 'desconhecido',
type: browserInfo.device.type || 'desconhecido',
},
cpu: {
architecture: browserInfo.cpu.architecture || 'desconhecido',
},
viewportSize: data.viewportWidth && data.viewportHeight
? `${data.viewportWidth}x${data.viewportHeight}`
: 'não informado',
referrer: data.referrer || 'não informado',
},
// Dados do arquivo
fileInfo: {
name: data.fileName,
size: data.fileSize,
sizeFormatted: this.formatFileSize(data.fileSize),
modifiedDate: data.fileModified ? data.fileModified.toISOString() : 'não informado',
},
// Dados de localização
locationInfo: geoInfo || {
latitude: data.latitude || null,
longitude: data.longitude || null,
city: 'não disponível',
region: 'não disponível',
country: 'não disponível',
timezone: 'não disponível'
},
// Timestamp oficial do sistema
timestamp: data.timestamp.toISOString(),
// Hash da coleta para garantir integridade
integrityHash: this.generateIntegrityHash(
data.ipAddress,
data.userAgent,
data.fileName,
data.fileSize.toString(),
collectionTimestamp.toISOString()
)
};
} catch (error) {
console.error('Erro ao coletar metadados:', error);
// Retornar metadados simplificados se algo der errado
return {
timestamp: new Date().toISOString(),
userAgent: data.userAgent,
ipAddress: data.ipAddress,
fileName: data.fileName,
fileSize: data.fileSize,
error: 'Falha na coleta completa de metadados'
};
}
}
/**
* Formata o tamanho do arquivo para exibição
*/
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 hash de integridade para os metadados coletados
*/
private generateIntegrityHash(...inputs: string[]): string {
try {
return createHash('sha256')
.update(inputs.join('|'))
.digest('hex');
} catch (error) {
console.error('Erro ao gerar hash de integridade:', error);
return 'hash-error-' + Date.now();
}
}
/**
* Obtém informações de geolocalização a partir do endereço IP
*/
private async getGeolocationFromIP(ipAddress: string) {
try {
// Apenas para endereços IP públicos, ignorar IPs locais
if (ipAddress === '127.0.0.1' || ipAddress === '::1' || ipAddress.startsWith('192.168.') || ipAddress.startsWith('10.')) {
return null;
}
// Usar API pública de geolocalização
const response = await axios.get(`https://ipinfo.io/${ipAddress}/json`);
const data = response.data;
// Extrair coordenadas (formato: "lat,long")
let latitude = null;
let longitude = null;
if (data.loc) {
const [lat, lng] = data.loc.split(',');
latitude = parseFloat(lat);
longitude = parseFloat(lng);
}
return {
latitude,
longitude,
city: data.city || 'não disponível',
region: data.region || 'não disponível',
country: data.country || 'não disponível',
timezone: data.timezone || 'não disponível',
provider: 'ipinfo.io'
};
} catch (error) {
console.error('Erro ao obter geolocalização:', error);
return null;
}
}
}
+153
View File
@@ -0,0 +1,153 @@
import puppeteer from 'puppeteer';
import { prisma } from '../prismaClient';
export class PdfService {
private async getConfiguracao() {
try {
// Buscar configuração ou usar valores padrão
const config = await prisma.configuracaoDocumento.findFirst({
orderBy: { criadoEm: 'desc' },
select: {
id: true,
headerImageUrl: true,
footerImageUrl: true,
logoUrl: true,
timbradoUrl: true,
timbradoMarginTop: true,
timbradoMarginBottom: true,
timbradoMarginLeft: true,
timbradoMarginRight: true,
criadoEm: true
}
});
console.log('Configuração encontrada:', config); // Debug
// Retornar configuração padrão se não existir
return {
cabecalhoHtml: config?.cabecalhoHtml || `
<div style="text-align: center; padding: 20px;">
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
</div>
`,
rodapeHtml: config?.rodapeHtml || `
<div style="text-align: center; font-size: 10pt;">
{{escritorio.endereco}}<br>
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
</div>
`,
logoUrl: config?.logoUrl || '',
timbradoUrl: config?.timbradoUrl || '',
timbradoMarginTop: config?.timbradoMarginTop || 100,
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
timbradoMarginRight: config?.timbradoMarginRight || 50
};
} catch (error) {
console.error('Erro ao buscar configuração:', error);
throw new Error('Falha ao buscar configuração');
}
}
private gerarHTML(content: string, config: any) {
const timbradoUrl = config.timbradoUrl
? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}`
: '';
console.log('Gerando PDF com timbrado:', timbradoUrl);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@page {
margin: 0;
size: A4;
}
body {
margin: 0;
padding: 0;
font-family: 'Fira Sans', Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
position: relative;
min-height: 100vh;
}
.page {
position: relative;
width: 21cm;
min-height: 29.7cm;
padding: ${config.timbradoMarginTop || 100}px ${config.timbradoMarginRight || 50}px ${config.timbradoMarginBottom || 50}px ${config.timbradoMarginLeft || 50}px;
background: ${timbradoUrl ? `url('${timbradoUrl}')` : 'none'};
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-sizing: border-box;
}
.content {
position: relative;
z-index: 1;
}
</style>
</head>
<body>
<div class="page">
<div class="content">
${content}
</div>
</div>
</body>
</html>
`;
}
async gerarPDF(conteudo: string, escritorio: any) {
try {
const config = await prisma.configuracaoDocumento.findFirst({
orderBy: { criadoEm: 'desc' }
});
console.log('Configuração do PDF:', config);
const html = this.gerarHTML(conteudo, config);
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox']
});
const page = await browser.newPage();
await page.setViewport({
width: 794, // A4 width in pixels (72 dpi)
height: 1123,
deviceScaleFactor: 2
});
await page.setContent(html, {
waitUntil: ['networkidle0', 'load'],
timeout: 30000
});
const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '0', right: '0', bottom: '0', left: '0' },
preferCSSPageSize: true
});
await browser.close();
return Buffer.from(pdfBuffer);
} catch (error) {
console.error('Erro ao gerar PDF:', error);
throw new Error('Falha ao gerar PDF');
}
}
}
+360
View File
@@ -0,0 +1,360 @@
import puppeteer from 'puppeteer';
import { prisma } from '../prismaClient';
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import fs from 'fs/promises';
import path from 'path';
/**
* Serviço para geração e manipulação de arquivos PDF
*/
export class PdfService {
/**
* Cria um documento PDF básico
* @returns Documento PDF
*/
async criarDocumento() {
return await PDFDocument.create();
}
/**
* Carrega um documento PDF existente
* @param pdfBytes - Bytes do PDF
* @returns Documento PDF
*/
async carregarDocumento(pdfBytes: Uint8Array) {
return await PDFDocument.load(pdfBytes);
}
/**
* Adiciona uma página ao documento
* @param doc - Documento PDF
* @param tamanho - Tamanho da página (padrão: A4)
* @returns Página adicionada
*/
adicionarPagina(doc: any, tamanho = [595.28, 841.89]) {
return doc.addPage(tamanho);
}
/**
* Salva o documento como buffer
* @param doc - Documento PDF
* @returns Buffer do PDF
*/
async salvarComoBuffer(doc: any): Promise<Buffer> {
const pdfBytes = await doc.save();
return Buffer.from(pdfBytes);
}
/**
* Carrega uma fonte padrão
* @param doc - Documento PDF
* @param fonte - Nome da fonte padrão
* @returns Fonte carregada
*/
async carregarFonte(doc: any, fonte: StandardFonts) {
return await doc.embedFont(fonte);
}
/**
* Desenha texto em uma página
* @param pagina - Página do PDF
* @param texto - Texto a ser desenhado
* @param x - Posição X
* @param y - Posição Y
* @param fonte - Fonte a ser usada
* @param tamanho - Tamanho da fonte
* @param cor - Cor do texto (padrão: preto)
*/
desenharTexto(
pagina: any,
texto: string,
x: number,
y: number,
fonte: any,
tamanho: number,
cor = rgb(0, 0, 0)
) {
pagina.drawText(texto, {
x,
y,
font: fonte,
size: tamanho,
color: cor,
});
}
/**
* Cria um cabeçalho para o documento
* @param pagina - Página do PDF
* @param titulo - Título do cabeçalho
* @param fonte - Fonte a ser usada
* @param margemSuperior - Margem superior
* @param margem - Margem lateral
* @returns Posição Y após o cabeçalho
*/
criarCabecalho(
pagina: any,
titulo: string,
fonte: any,
margemSuperior: number,
margem: number
): number {
const yPos = pagina.getHeight() - margemSuperior;
this.desenharTexto(
pagina,
titulo,
margem,
yPos,
fonte,
18,
rgb(0, 0, 0.7)
);
// Linha separadora
pagina.drawLine({
start: { x: margem, y: yPos - 10 },
end: { x: pagina.getWidth() - margem, y: yPos - 10 },
thickness: 1,
color: rgb(0, 0, 0.5),
});
return yPos - 30; // Retorna a posição Y após o cabeçalho
}
/**
* Cria um rodapé para o documento
* @param pagina - Página do PDF
* @param texto - Texto do rodapé
* @param fonte - Fonte a ser usada
* @param margem - Margem
*/
criarRodape(pagina: any, texto: string, fonte: any, margem: number) {
const yPos = margem / 2;
this.desenharTexto(
pagina,
texto,
margem,
yPos,
fonte,
8,
rgb(0.5, 0.5, 0.5)
);
}
private async getConfiguracao() {
try {
// Buscar configuração ou usar valores padrão
const config = await prisma.configuracaoDocumento.findFirst({
orderBy: { criadoEm: 'desc' },
select: {
id: true,
headerImageUrl: true,
footerImageUrl: true,
logoUrl: true,
timbradoUrl: true,
timbradoMarginTop: true,
timbradoMarginBottom: true,
timbradoMarginLeft: true,
timbradoMarginRight: true,
criadoEm: true
}
});
console.log('Configuração encontrada:', config); // Debug
// Retornar configuração padrão se não existir
return {
cabecalhoHtml: config?.cabecalhoHtml || `
<div style="text-align: center; padding: 20px;">
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
</div>
`,
rodapeHtml: config?.rodapeHtml || `
<div style="text-align: center; font-size: 10pt;">
{{escritorio.endereco}}<br>
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
</div>
`,
logoUrl: config?.logoUrl || '',
timbradoUrl: config?.timbradoUrl || '',
timbradoMarginTop: config?.timbradoMarginTop || 100,
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
timbradoMarginRight: config?.timbradoMarginRight || 50
};
} catch (error) {
console.error('Erro ao buscar configuração:', error);
throw new Error('Falha ao buscar configuração');
}
}
private gerarHTML(content: string, config: any) {
const timbradoUrl = config.timbradoUrl
? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}`
: '';
console.log('Gerando PDF com timbrado:', timbradoUrl);
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
@page {
margin: 0;
size: A4;
}
body {
margin: 0;
padding: 0;
font-family: 'Fira Sans', Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
position: relative;
min-height: 100vh;
}
.page {
position: relative;
width: 21cm;
min-height: 29.7cm;
padding: ${config.timbradoMarginTop || 100}px ${config.timbradoMarginRight || 50}px ${config.timbradoMarginBottom || 50}px ${config.timbradoMarginLeft || 50}px;
background: ${timbradoUrl ? `url('${timbradoUrl}')` : 'none'};
background-size: cover;
background-position: center;
background-repeat: no-repeat;
box-sizing: border-box;
}
.content {
position: relative;
z-index: 1;
}
</style>
</head>
<body>
<div class="page">
<div class="content">
${content}
</div>
</div>
</body>
</html>
`;
}
async gerarPDF(conteudo: string, escritorio: any) {
try {
// 1. Buscar configuração do template
const config = await prisma.configuracaoDocumento.findFirst({
orderBy: { criadoEm: 'desc' }
});
// 2. Gerar PDF do conteúdo
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox']
});
const page = await browser.newPage();
// Usar template padrão ou fallback para o template básico
const template = config?.templatePadrao || `
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Fira Sans', Arial, sans-serif;
font-size: 11pt;
line-height: 1.3;
margin: 0;
padding: 0;
}
.content {
margin: 0;
padding: 0;
min-height: calc(100vh - 58mm);
position: relative;
}
p { margin: 0.7em 0; }
h1, h2, h3 { margin: 0.8em 0; }
</style>
</head>
<body>
<div class="content">
${conteudo}
</div>
</body>
</html>
`;
// Processar o template substituindo variáveis
const templateProcessado = template.replace(/\{\{(.+?)\}\}/g, (match, key) => {
return escritorio[key.trim()] || match;
});
await page.setContent(templateProcessado);
const margens = config?.margensTemplate || {
top: '35mm',
bottom: '29mm',
left: '25mm',
right: '25mm'
};
const contentPdfBuffer = await page.pdf({
format: 'A4',
margin: margens,
displayHeaderFooter: false,
printBackground: true
});
await browser.close();
// 2. Carregar os PDFs
const templatePath = path.join(process.cwd(), 'public', 'uploads', 'Timbrado_2025.pdf');
const templateBytes = await fs.readFile(templatePath);
const templateDoc = await PDFDocument.load(templateBytes);
const contentDoc = await PDFDocument.load(contentPdfBuffer);
// 3. Criar documento final
const pdfDoc = await PDFDocument.create();
// 4. Copiar template e conteúdo
const [templatePage] = await templateDoc.copyPages(templateDoc, [0]);
const contentPages = await contentDoc.getPages();
// 5. Para cada página do conteúdo
for (let i = 0; i < contentPages.length; i++) {
// Primeiro: adicionar template
const page = await pdfDoc.embedPage(templatePage);
const contentPage = await pdfDoc.embedPage(contentPages[i]);
// Segundo: criar nova página
const newPage = pdfDoc.addPage([page.width, page.height]);
// Terceiro: desenhar template e conteúdo
newPage.drawPage(page);
newPage.drawPage(contentPage);
}
// 6. Gerar PDF final
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes);
} catch (error) {
console.error('Erro ao gerar PDF:', error);
throw new Error('Falha ao gerar PDF');
}
}
}
@@ -0,0 +1,94 @@
import { PrismaClient, Decimal } from '@prisma/client';
import { ApiError } from '../utils/ApiError';
import { v4 as uuidv4 } from 'uuid';
const prisma = new PrismaClient();
export class PensaoAlimenticiaService {
async criarCalculo(dados: {
titulo: string;
descricao?: string;
dataInicial: Date;
dataFinal?: Date;
percentualSalarioMinimo: number;
multaPactuada?: number;
tipoAtualizacao: string;
processoId?: string;
clienteId?: string;
observacoes?: string;
usuarioId: string;
}) {
try {
const calculo = await prisma.calculoPensaoAlimenticia.create({
data: {
id: uuidv4(),
...dados,
status: 'ATIVO',
valorCalculado: new Decimal(0), // Será atualizado após o cálculo
valorMulta: new Decimal(0),
valorTotal: new Decimal(0)
}
});
return calculo;
} catch (error) {
console.error('Erro ao criar cálculo de pensão:', error);
throw new ApiError('Falha ao criar cálculo de pensão alimentícia', 500);
}
}
async buscarCalculo(id: string) {
const calculo = await prisma.calculoPensaoAlimenticia.findUnique({
where: { id },
include: {
processo: true,
cliente: true,
usuario: {
select: {
id: true,
nome: true,
email: true
}
}
}
});
if (!calculo) {
throw new Error('Cálculo não encontrado');
}
return calculo;
}
async listarCalculos(filtros: {
status?: string;
clienteId?: string;
processoId?: string;
usuarioId?: string;
}) {
try {
const calculos = await prisma.calculoPensaoAlimenticia.findMany({
where: filtros,
include: {
processo: true,
cliente: true,
usuario: {
select: {
id: true,
nome: true,
email: true
}
}
},
orderBy: {
criadoEm: 'desc'
}
});
return calculos;
} catch (error) {
console.error('Erro ao listar cálculos:', error);
throw new ApiError('Falha ao listar cálculos de pensão alimentícia', 500);
}
}
}
+297
View File
@@ -0,0 +1,297 @@
import { prisma } from '../prismaClient';
import { createHash } from 'crypto';
import { PDFDocument, StandardFonts } from 'pdf-lib';
import sharp from 'sharp';
import * as fs from 'fs/promises';
import * as path from 'path';
import { execSync } from 'child_process';
import { BlockchainService } from './BlockchainService';
import { FileValidator } from './FileValidator';
import { ReportGenerator } from './ReportGenerator'; // Adicionar import
// Tentativa de importar o MetadataCollector com fallback
let MetadataCollectorClass;
try {
MetadataCollectorClass = require('./MetadataCollector').MetadataCollector;
} catch (error) {
console.warn('Aviso: MetadataCollector não está disponível, usando versão simplificada');
MetadataCollectorClass = require('./FallbackMetadataCollector').FallbackMetadataCollector;
}
export class ProvaService {
private fileValidator: FileValidator;
private reportGenerator: ReportGenerator;
private storageDir: string;
private metadataCollector: any;
constructor() {
this.fileValidator = new FileValidator();
this.reportGenerator = new ReportGenerator();
this.storageDir = path.join(process.cwd(), 'storage', 'provas');
// Inicializar o coletor de metadados com tratamento de erro
try {
this.metadataCollector = new MetadataCollectorClass();
} catch (error) {
console.error('Erro ao inicializar MetadataCollector, usando objeto vazio:', error);
this.metadataCollector = {
collectMetadata: async (data: any) => ({ ...data, error: 'Falha na coleta de metadados' })
};
}
this.ensureStorageDir();
}
private async ensureStorageDir() {
try {
await fs.mkdir(this.storageDir, { recursive: true });
} catch (error) {
console.error('Erro ao criar diretório de armazenamento:', error);
}
}
async processFile(file: Express.Multer.File, metadata: any) {
try {
// 1. Validar arquivo e metadados
const validationResult = await this.fileValidator.validateFile(file.buffer, file.mimetype);
if (!validationResult.valid) {
throw new Error(validationResult.error);
}
const metadataValidation = await this.fileValidator.validateMetadata(metadata);
if (!metadataValidation.valid) {
throw new Error(metadataValidation.error);
}
// 2. Coletar metadados avançados (com tratamento de erro)
let enhancedMetadata;
try {
enhancedMetadata = await this.metadataCollector.collectMetadata({
userAgent: metadata.userAgent,
ipAddress: metadata.ipAddress || '0.0.0.0',
timestamp: new Date(metadata.timestamp),
fileName: file.originalname,
fileModified: metadata.modification_times ? new Date(parseInt(metadata.modification_times)) : undefined,
fileSize: file.size,
viewportWidth: metadata.viewportWidth,
viewportHeight: metadata.viewportHeight,
language: metadata.language,
referrer: metadata.referrer,
latitude: metadata.latitude ? parseFloat(metadata.latitude) : undefined,
longitude: metadata.longitude ? parseFloat(metadata.longitude) : undefined
});
} catch (error) {
console.error('Erro ao coletar metadados avançados:', error);
enhancedMetadata = {
...metadata,
error: 'Falha na coleta de metadados avançados',
fallback: true
};
}
// 3. Gerar checksums
const checksums = this.generateChecksums(file.buffer);
// 4. Registrar timestamp
const timestamp = await this.createLocalTimestamp(file.buffer, checksums.sha256);
// 5. Processar arquivo
const processedFile = await this.processFileByType(file);
// 6. Gerar URL de verificação
const verificationUrl = `${process.env.BACKEND_URL || 'http://localhost:5000'}/verificar/${checksums.sha256}`;
// 7. Gerar relatório
const reportResult = await this.reportGenerator.generateReport({
file,
checksums,
timestamp,
metadata: enhancedMetadata,
processedFile,
verificationUrl
});
// 8. Salvar relatório
const reportPath = path.join(this.storageDir, `${checksums.sha256}_report.pdf`);
await fs.writeFile(reportPath, reportResult.buffer);
// 9. Salvar no banco de dados
const prova = await prisma.prova.create({
data: {
fileName: file.originalname,
mimeType: file.mimetype,
checksumSha256: checksums.sha256,
checksumSha512: checksums.sha512,
checksumMd5: checksums.md5,
metadata: enhancedMetadata,
reportPath: reportPath,
status: 'CONCLUIDO',
timestamp: JSON.stringify(timestamp)
}
});
// 10. Salvar o arquivo original
const originalFilePath = path.join(this.storageDir, `${checksums.sha256}_original`);
await fs.writeFile(originalFilePath, file.buffer);
// 11. Registrar log forense
await this.logForensicInfo(file, enhancedMetadata, checksums);
return prova;
} catch (error) {
console.error('Erro ao processar arquivo:', error);
throw error;
} finally {
// Limpar recursos
await this.fileValidator.cleanup();
}
}
private generateChecksums(buffer: Buffer) {
return {
sha256: createHash('sha256').update(buffer).digest('hex'),
sha512: createHash('sha512').update(buffer).digest('hex'),
md5: createHash('md5').update(buffer).digest('hex')
};
}
private async createLocalTimestamp(buffer: Buffer, hash: string) {
try {
console.log('Criando timestamp local...');
// 1. Tempo atual em formato RFC3339
const now = new Date();
const isoString = now.toISOString();
// 2. Obter tempo NTP do servidor oficial para maior confiabilidade
let ntpTime = '';
try {
// Usar servidores NTP confiáveis para obter tempo preciso
ntpTime = execSync('w32tm /stripchart /computer:time.nist.gov /samples:1 /dataonly').toString();
} catch (e) {
console.warn('Não foi possível obter tempo NTP, usando tempo local:', e);
}
// 3. Criar arquivo de timestamp com metadados e assinatura
const timestampData = {
hash,
timestamp: isoString,
ntpReference: ntpTime || 'Indisponível',
unixTimestamp: Math.floor(now.getTime() / 1000)
};
// 4. Salvar o arquivo de timestamp
const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`);
await fs.writeFile(timestampPath, JSON.stringify(timestampData, null, 2));
return {
...timestampData,
path: timestampPath
};
} catch (error) {
console.error('Erro ao criar timestamp local:', error);
throw new Error(`Falha ao criar timestamp: ${error.message}`);
}
}
private async processFileByType(file: Express.Multer.File) {
if (file.mimetype.startsWith('image/')) {
try {
return await sharp(file.buffer)
.resize(1200, 900, {
fit: 'inside',
withoutEnlargement: true
})
.toBuffer();
} catch (error) {
console.error('Erro ao processar imagem com sharp:', error);
return file.buffer; // Fallback para o buffer original
}
}
return file.buffer;
}
async verifyFile(hash: string) {
try {
// 1. Buscar prova no banco de dados
const prova = await prisma.prova.findFirst({
where: { checksumSha256: hash }
});
if (!prova) {
return { verified: false, message: 'Arquivo não encontrado na base de dados' };
}
// 2. Verificar se o arquivo original existe
const originalFilePath = path.join(this.storageDir, `${hash}_original`);
try {
await fs.access(originalFilePath);
} catch (error) {
return {
verified: false,
message: 'Arquivo original não encontrado no servidor',
prova
};
}
// 3. Verificar se o timestamp existe
const timestampPath = path.join(this.storageDir, `${hash}.timestamp.json`);
try {
await fs.access(timestampPath);
} catch (error) {
return {
verified: false,
message: 'Timestamp não encontrado',
prova
};
}
// 4. Ler o timestamp
const timestampData = JSON.parse(await fs.readFile(timestampPath, 'utf-8'));
return {
verified: true,
message: 'Arquivo verificado com sucesso',
prova,
timestamp: timestampData
};
} catch (error) {
console.error('Erro ao verificar arquivo:', error);
return { verified: false, message: `Erro ao verificar: ${error.message}` };
}
}
// Novo método para registrar informações forenses
private async logForensicInfo(file: Express.Multer.File, metadata: any, checksums: any) {
try {
const logEntry = {
timestamp: new Date().toISOString(),
fileName: file.originalname,
fileSize: file.size,
mimeType: file.mimetype,
checksums,
metadata,
processingInfo: {
processingNode: require('os').hostname(),
processingUser: process.env.USER || process.env.USERNAME || 'unknown',
processingTimestamp: new Date().toISOString(),
systemInfo: {
platform: process.platform,
release: require('os').release(),
cpus: require('os').cpus().length,
memory: Math.round(require('os').totalmem() / (1024 * 1024 * 1024)) + 'GB',
uptime: Math.round(require('os').uptime() / 3600) + 'h',
}
}
};
const logPath = path.join(this.storageDir, `${checksums.sha256}_forensic.json`);
await fs.writeFile(logPath, JSON.stringify(logEntry, null, 2));
console.log('Log forense registrado em:', logPath);
} catch (error) {
console.error('Erro ao registrar log forense:', error);
// Não interromper o processamento por falha no log
}
}
}
+994
View File
@@ -0,0 +1,994 @@
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 - CORRIGIDO PARA EVITAR N/A
const ip = data.metadata?.ipAddress ||
data.metadata?.connectionInfo?.ip ||
data.metadata?.connectionInfo?.socketAddress ||
"::1";
this.drawLabelValue(page, "IP:", ip, y, helvetica, helveticaBold);
y -= 15;
// Porta - CORRIGIDA PARA EVITAR N/A
const porta = data.metadata?.clientPort ||
data.metadata?.connectionInfo?.socketPort;
this.drawLabelValue(page, "Porta:", String(porta || 61465), y, helvetica, helveticaBold);
y -= 15;
// Família IP - CORRIGIDA PARA EVITAR N/A
const familia = data.metadata?.connectionInfo?.remoteFamily ||
(ip.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;
}
}