Files
advx/src/server/services/IndiceMonetarioService.ts
T
2025-04-10 23:07:21 -03:00

932 lines
27 KiB
TypeScript

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