senal88's picture
chore: deploy web_comercial from monorepo
7a818d6 verified
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,
},
};
}