Spaces:
Runtime error
Runtime error
| 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, | |
| }, | |
| }; | |
| } | |