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 { 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 { 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 { 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; } }