import { SCP_CLIENTS_DEFAULT, SCP_FLOW_PATTERNS_DEFAULT } from "./scpDefaults.js"; function ensurePositiveNumber(name, value) { if (typeof value !== "number" || Number.isNaN(value) || value <= 0) { throw new Error(`${name} deve ser um numero maior que zero`); } } function ensureNonNegativeNumber(name, value) { if (typeof value !== "number" || Number.isNaN(value) || value < 0) { throw new Error(`${name} deve ser um numero nao negativo`); } } function ensurePercent(name, value) { if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) { throw new Error(`${name} deve estar entre 0 e 1`); } } function ensureNumberArray(name, value) { if (!Array.isArray(value) || value.length === 0) { throw new Error(`${name} deve ser uma lista com ao menos um valor`); } for (const item of value) { if (typeof item !== "number" || Number.isNaN(item)) { throw new Error(`${name} deve conter apenas numeros`); } } } function ensureInteger(name, value) { if (!Number.isInteger(value)) { throw new Error(`${name} deve ser inteiro`); } } function addMonths(isoDate, deltaMonths) { const [year, month, day] = isoDate.split("-").map(Number); const base = new Date(Date.UTC(year, month - 1, day || 1)); base.setUTCMonth(base.getUTCMonth() + deltaMonths); const y = base.getUTCFullYear(); const m = String(base.getUTCMonth() + 1).padStart(2, "0"); const d = String(base.getUTCDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } function validateInput(input) { if (!input || typeof input !== "object") { throw new Error("input invalido"); } ensurePositiveNumber("credito_desejado", input.credito_desejado); if (!input.parametros || typeof input.parametros !== "object") { throw new Error("parametros invalidos"); } const p = input.parametros; ensurePositiveNumber("prazo_meses", p.prazo_meses); ensureNonNegativeNumber("taxa_administracao_total", p.taxa_administracao_total); ensureNonNegativeNumber("fundo_reserva_mensal", p.fundo_reserva_mensal); ensureNonNegativeNumber("seguro_prestamista", p.seguro_prestamista); if (input.tem_lance && input.lance_percentual !== undefined) { ensureNonNegativeNumber("lance_percentual", input.lance_percentual); } } export function listarPerfisFluxoScpDefault() { return { clients: SCP_CLIENTS_DEFAULT.map((client) => ({ ...client })), patterns: SCP_FLOW_PATTERNS_DEFAULT.map((pattern) => ({ id: pattern.id, nome: pattern.nome, descricao: pattern.descricao, pontos: pattern.pontos.length, inicio: pattern.pontos[0]?.periodo, fim: pattern.pontos[pattern.pontos.length - 1]?.periodo, })), }; } function findPattern(patternId) { return SCP_FLOW_PATTERNS_DEFAULT.find((pattern) => pattern.id === patternId); } function findClient(clientId) { return SCP_CLIENTS_DEFAULT.find((client) => client.client_id === clientId); } function validateScpFlowInput(input) { if (!input || typeof input !== "object") { throw new Error("input de fluxo scp invalido"); } if (typeof input.client_id !== "string" || !input.client_id.trim()) { throw new Error("client_id obrigatorio"); } if (input.pattern_id !== undefined && typeof input.pattern_id !== "string") { throw new Error("pattern_id deve ser string"); } if (input.escala_fluxo !== undefined) { ensurePositiveNumber("escala_fluxo", input.escala_fluxo); } if (input.deslocamento_meses !== undefined) { ensureInteger("deslocamento_meses", input.deslocamento_meses); } } export function montarFluxoClienteScp(input) { validateScpFlowInput(input); const client = findClient(input.client_id); if (!client) { throw new Error(`cliente nao encontrado: ${input.client_id}`); } const selectedPatternId = input.pattern_id || client.default_pattern_id; const pattern = findPattern(selectedPatternId); if (!pattern) { throw new Error(`padrao de fluxo nao encontrado: ${selectedPatternId}`); } const escalaFluxo = input.escala_fluxo !== undefined ? input.escala_fluxo : 1; const deslocamentoMeses = input.deslocamento_meses !== undefined ? input.deslocamento_meses : 0; const novoClientId = input.novo_client_id && String(input.novo_client_id).trim() ? String(input.novo_client_id).trim() : client.client_id; const novoClientName = input.novo_client_name && String(input.novo_client_name).trim() ? String(input.novo_client_name).trim() : client.client_name; const pontos = pattern.pontos.map((point) => ({ periodo: deslocamentoMeses === 0 ? point.periodo : addMonths(point.periodo, deslocamentoMeses), fluxo: point.fluxo * escalaFluxo, })); const fluxosMensais = pontos.map((point) => point.fluxo); return { client_id: novoClientId, client_name: novoClientName, source_client_id: client.client_id, pattern_id: pattern.id, pattern_name: pattern.nome, escala_fluxo: escalaFluxo, deslocamento_meses: deslocamentoMeses, pontos, fluxos_mensais: fluxosMensais, }; } export function calcularSimulacao(input) { validateInput(input); const { credito_desejado, parametros, tem_lance, lance_percentual } = input; const { prazo_meses, taxa_administracao_total, fundo_reserva_mensal, seguro_prestamista, } = parametros; const fcMensal = 1.0 / prazo_meses; const taMensal = taxa_administracao_total / prazo_meses; const valorFC = credito_desejado * fcMensal; const valorTA = credito_desejado * taMensal; const valorFR = credito_desejado * fundo_reserva_mensal; const valorBaseSeguro = credito_desejado + credito_desejado * taxa_administracao_total; const taxaSeguroMensal = seguro_prestamista / prazo_meses; const valorSeguro = valorBaseSeguro * taxaSeguroMensal; const parcelaIntegral = { fundo_comum: valorFC, taxa_administracao: valorTA, fundo_reserva: valorFR, seguro: valorSeguro, total: valorFC + valorTA + valorFR + valorSeguro, }; const fatorReducao = 0.7; const valorFCReduzido = valorFC * fatorReducao; const parcelaReduzida = { fundo_comum: valorFCReduzido, taxa_administracao: valorTA, fundo_reserva: valorFR, seguro: valorSeguro, total: valorFCReduzido + valorTA + valorFR + valorSeguro, }; let valorLance = 0; let prazoAposLance = prazo_meses; if (tem_lance && lance_percentual && lance_percentual > 0) { valorLance = credito_desejado * lance_percentual; const numeroParcelasAntecipadas = Math.floor(valorLance / parcelaIntegral.total); prazoAposLance = Math.max(1, prazo_meses - numeroParcelasAntecipadas); } return { credito: credito_desejado, prazo: prazo_meses, parcela_integral: parcelaIntegral, parcela_reduzida: parcelaReduzida, valor_lance: tem_lance ? valorLance : undefined, prazo_apos_lance: tem_lance ? prazoAposLance : undefined, }; } function npv(rate, cashflows) { let value = 0; for (let i = 0; i < cashflows.length; i += 1) { value += cashflows[i] / (1 + rate) ** i; } return value; } function hasSignChange(cashflows) { let hasPositive = false; let hasNegative = false; for (const value of cashflows) { if (value > 0) { hasPositive = true; } if (value < 0) { hasNegative = true; } if (hasPositive && hasNegative) { return true; } } return false; } function calculateIrrMonthly(cashflows) { if (!hasSignChange(cashflows)) { return null; } let low = -0.9999; let high = 10; let npvLow = npv(low, cashflows); let npvHigh = npv(high, cashflows); if (npvLow === 0) { return low; } if (npvHigh === 0) { return high; } let attempts = 0; while (npvLow * npvHigh > 0 && attempts < 50) { high *= 2; npvHigh = npv(high, cashflows); attempts += 1; } if (npvLow * npvHigh > 0) { return null; } for (let i = 0; i < 200; i += 1) { const mid = (low + high) / 2; const npvMid = npv(mid, cashflows); if (Math.abs(npvMid) < 1e-8) { return mid; } if (npvLow * npvMid < 0) { high = mid; npvHigh = npvMid; } else { low = mid; npvLow = npvMid; } } return (low + high) / 2; } function validateSecondaryInput(input) { if (!input || typeof input !== "object") { throw new Error("input secundario invalido"); } ensureNumberArray("fluxos_mensais", input.fluxos_mensais); if (input.portabilidade_percentual_contemplada !== undefined) { ensurePercent( "portabilidade_percentual_contemplada", input.portabilidade_percentual_contemplada ); } if (input.portabilidade_percentual_nao_contemplada !== undefined) { ensurePercent( "portabilidade_percentual_nao_contemplada", input.portabilidade_percentual_nao_contemplada ); } if (input.fee_percentual !== undefined) { ensurePercent("fee_percentual", input.fee_percentual); } if (input.percentual_cartas_parcela_reduzida !== undefined) { ensurePercent( "percentual_cartas_parcela_reduzida", input.percentual_cartas_parcela_reduzida ); } if (input.volume_contratado !== undefined) { ensureNonNegativeNumber("volume_contratado", input.volume_contratado); } if (input.meses_pos_ultima_contemplacao !== undefined) { ensureNonNegativeNumber( "meses_pos_ultima_contemplacao", input.meses_pos_ultima_contemplacao ); } if (input.transacoes_portabilidade !== undefined) { if (!Array.isArray(input.transacoes_portabilidade)) { throw new Error("transacoes_portabilidade deve ser uma lista"); } for (const transacao of input.transacoes_portabilidade) { if (!transacao || typeof transacao !== "object") { throw new Error("transacoes_portabilidade contem item invalido"); } ensureNonNegativeNumber("transacao.valor", transacao.valor); if (typeof transacao.contemplada !== "boolean") { throw new Error("transacao.contemplada deve ser boolean"); } } } } export function calcularIndicadoresSecundario(input) { validateSecondaryInput(input); const portabilidadeContemplada = input.portabilidade_percentual_contemplada !== undefined ? input.portabilidade_percentual_contemplada : 0.01; const portabilidadeNaoContemplada = input.portabilidade_percentual_nao_contemplada !== undefined ? input.portabilidade_percentual_nao_contemplada : 0.005; const feePercentual = input.fee_percentual !== undefined ? input.fee_percentual : 0; const volumeContratado = input.volume_contratado !== undefined ? input.volume_contratado : 0; const percentualCartasParcelaReduzida = input.percentual_cartas_parcela_reduzida !== undefined ? input.percentual_cartas_parcela_reduzida : 0; const mesesPosUltimaContemplacao = input.meses_pos_ultima_contemplacao !== undefined ? input.meses_pos_ultima_contemplacao : 0; let custoPortabilidade = 0; const transacoes = input.transacoes_portabilidade || []; for (const transacao of transacoes) { const taxa = transacao.contemplada ? portabilidadeContemplada : portabilidadeNaoContemplada; custoPortabilidade += transacao.valor * taxa; } const fatorFeeVolume = (1 - percentualCartasParcelaReduzida) + percentualCartasParcelaReduzida * 0.7; const custoFeeParcelaReduzida = volumeContratado * fatorFeeVolume * feePercentual; const custoTotalDeducoes = custoPortabilidade + custoFeeParcelaReduzida; const fluxosAjustados = [...input.fluxos_mensais]; fluxosAjustados[0] -= custoTotalDeducoes; let acumulado = 0; let caixaMaximoNegativo = 0; let paybackBase = null; for (let i = 0; i < fluxosAjustados.length; i += 1) { acumulado += fluxosAjustados[i]; if (acumulado < caixaMaximoNegativo) { caixaMaximoNegativo = acumulado; } if (paybackBase === null && acumulado >= 0) { paybackBase = i + 1; } } const tirMensal = calculateIrrMonthly(fluxosAjustados); const tirAnual = tirMensal === null ? null : (1 + tirMensal) ** 12 - 1; const paybackMeses = paybackBase === null ? null : paybackBase + mesesPosUltimaContemplacao; return { fluxo_ajustado: fluxosAjustados, custo_portabilidade: custoPortabilidade, custo_fee_parcela_reduzida: custoFeeParcelaReduzida, custo_total_deducoes: custoTotalDeducoes, caixa_maximo_negativo: caixaMaximoNegativo, payback_meses: paybackMeses, tir_mensal: tirMensal, tir_anual: tirAnual, regras_aplicadas: { portabilidade_contemplada: portabilidadeContemplada, portabilidade_nao_contemplada: portabilidadeNaoContemplada, sem_correcao_antes_contemplacao: input.sem_correcao_antes_contemplacao !== undefined ? Boolean(input.sem_correcao_antes_contemplacao) : true, fee_sobre_volume_reduzido_70: true, }, }; } export function simularSecundarioComPerfilScp(input) { if (!input || typeof input !== "object") { throw new Error("input de simulacao scp invalido"); } const fluxoCliente = montarFluxoClienteScp({ client_id: input.client_id, pattern_id: input.pattern_id, escala_fluxo: input.escala_fluxo, deslocamento_meses: input.deslocamento_meses, novo_client_id: input.novo_client_id, novo_client_name: input.novo_client_name, }); const simulacao = calcularIndicadoresSecundario({ fluxos_mensais: fluxoCliente.fluxos_mensais, transacoes_portabilidade: input.transacoes_portabilidade, portabilidade_percentual_contemplada: input.portabilidade_percentual_contemplada, portabilidade_percentual_nao_contemplada: input.portabilidade_percentual_nao_contemplada, volume_contratado: input.volume_contratado, fee_percentual: input.fee_percentual, percentual_cartas_parcela_reduzida: input.percentual_cartas_parcela_reduzida, meses_pos_ultima_contemplacao: input.meses_pos_ultima_contemplacao, sem_correcao_antes_contemplacao: input.sem_correcao_antes_contemplacao, }); return { ...simulacao, fluxo_cliente: fluxoCliente, }; } function validateConversaoInput(input) { if (!input || typeof input !== "object") { throw new Error("input de conversao invalido"); } if (input.valor_imovel_base !== undefined) { ensurePositiveNumber("valor_imovel_base", input.valor_imovel_base); } if (input.desconto_imovel_percentual !== undefined) { ensurePercent("desconto_imovel_percentual", input.desconto_imovel_percentual); } if (input.percentual_entrada_imovel !== undefined) { ensurePercent("percentual_entrada_imovel", input.percentual_entrada_imovel); } if (input.agio_consorcio_percentual !== undefined) { ensurePercent("agio_consorcio_percentual", input.agio_consorcio_percentual); } if (input.taxa_custos_transacao_percentual !== undefined) { ensurePercent( "taxa_custos_transacao_percentual", input.taxa_custos_transacao_percentual ); } } export function simularModoConversaoScpConsorcio(input) { validateConversaoInput(input); const secundario = simularSecundarioComPerfilScp(input); const valorImovelBase = input.valor_imovel_base !== undefined ? input.valor_imovel_base : 500000; const descontoImovelPercentual = input.desconto_imovel_percentual !== undefined ? input.desconto_imovel_percentual : 0.15; const percentualEntradaImovel = input.percentual_entrada_imovel !== undefined ? input.percentual_entrada_imovel : 0.2; const agioConsorcioPercentual = input.agio_consorcio_percentual !== undefined ? input.agio_consorcio_percentual : 0.12; const taxaCustosTransacaoPercentual = input.taxa_custos_transacao_percentual !== undefined ? input.taxa_custos_transacao_percentual : 0; const valorImovelComDesconto = valorImovelBase * (1 - descontoImovelPercentual); const valorEntrada = valorImovelComDesconto * percentualEntradaImovel; const valorCreditoConsorcio = valorImovelComDesconto - valorEntrada; const valorCustosTransacao = valorImovelComDesconto * taxaCustosTransacaoPercentual; const investimentoTotalConversao = valorEntrada + valorCustosTransacao + secundario.custo_total_deducoes; const valorSaidaConsorcioSecundario = valorCreditoConsorcio * (1 + agioConsorcioPercentual); const lucroProjetadoConversao = valorSaidaConsorcioSecundario - investimentoTotalConversao; const roiProjetadoConversao = investimentoTotalConversao > 0 ? lucroProjetadoConversao / investimentoTotalConversao : null; return { ...secundario, modo_conversao: { natureza_cenario: "fechado_customizavel", observacao: "Cenario de conversao SCP para compra de imovel com desconto e operacao no secundario.", valor_imovel_base: valorImovelBase, desconto_imovel_percentual: descontoImovelPercentual, percentual_entrada_imovel: percentualEntradaImovel, agio_consorcio_percentual: agioConsorcioPercentual, taxa_custos_transacao_percentual: taxaCustosTransacaoPercentual, valor_imovel_com_desconto: valorImovelComDesconto, valor_entrada_imovel: valorEntrada, valor_credito_consorcio: valorCreditoConsorcio, valor_custos_transacao: valorCustosTransacao, investimento_total_conversao: investimentoTotalConversao, valor_saida_consorcio_secundario: valorSaidaConsorcioSecundario, lucro_projetado_conversao: lucroProjetadoConversao, roi_projetado_conversao: roiProjetadoConversao, }, }; }