/** * Opciones de credito sobre el esquema FRASA 2026 (Excel del cliente, 4 * propuestas): Crédito Bancario, Crédito Infonavit-Cofinavit, Semi Contado * y Contado. Cada opcion aplica su descuento (0/0/1%/2%) al precio de lista, * recalcula el esquema FRASA 2026 sobre el precio final y estima la * mensualidad del credito hipotecario restante (saldo a financiar = 84.5% * del precio final, una vez saldado el 15.5% del esquema). * * Módulo puro (sin UI, sin API) para probarlo a fondo. */ import { PCT_ENGANCHE_MAXIMO, calcularEsquemaFrasa, type EsquemaFrasaResultado } from './esquema-frasa' export type TipoCredito = | 'CREDITO_BANCARIO' | 'CREDITO_INFONAVIT_COFINAVIT' | 'SEMI_CONTADO' | 'CONTADO' export interface AjustesCredito { /** Tasa anual nominal (0.105 = 10.5%) */ tasaAnual: number /** Plazo en años (entero, 1..35) */ plazoAnos: number /** * Solo SEMI_CONTADO: fraccion (0..1) del saldo a financiar que el cliente * liquida de contado a la firma; el resto va al credito hipotecario. */ pctContado?: number } /** Descuentos sobre el precio de lista definidos en el Excel del cliente */ export const DESCUENTO_POR_TIPO: Record = { CREDITO_BANCARIO: 0, CREDITO_INFONAVIT_COFINAVIT: 0, SEMI_CONTADO: 0.01, CONTADO: 0.02, } export const AJUSTES_DEFAULT: Record = { CREDITO_BANCARIO: { tasaAnual: 0.105, plazoAnos: 20 }, CREDITO_INFONAVIT_COFINAVIT: { tasaAnual: 0.105, plazoAnos: 25 }, SEMI_CONTADO: { tasaAnual: 0.105, plazoAnos: 20, pctContado: 0.5 }, // Valores dummy: el contado no usa credito CONTADO: { tasaAnual: 0, plazoAnos: 1 }, } /** Orden de presentacion de las 4 opciones (el del Excel del cliente) */ export const TIPOS_ORDEN: readonly TipoCredito[] = [ 'CREDITO_BANCARIO', 'CREDITO_INFONAVIT_COFINAVIT', 'SEMI_CONTADO', 'CONTADO', ] export const TITULO_POR_TIPO: Record = { CREDITO_BANCARIO: 'Crédito Bancario', CREDITO_INFONAVIT_COFINAVIT: 'Crédito Infonavit-Cofinavit', SEMI_CONTADO: 'Semi Contado', CONTADO: 'Contado', } const round2 = (x: number): number => Number(x.toFixed(2)) export interface EntradaOpcion { /** Precio de lista de la unidad (sin descuento) */ precioLista: number apartado: number numMensualidades: number escrituracionDiferida: boolean /** Enganche adicional (monto absoluto, >= 0): se acota al 90% del anticipo */ engancheAdicional?: number tipo: TipoCredito ajustes: AjustesCredito } export interface ResultadoOpcionCredito { tipo: TipoCredito titulo: string /** Fraccion de descuento sobre el precio de lista (0/0.01/0.02) */ descuento: number montoDescuento: number /** precioLista − montoDescuento (base del esquema FRASA 2026) */ precioFinal: number /** Esquema FRASA 2026 calculado sobre el precio final (sin redondear) */ esquema: EsquemaFrasaResultado /** precioFinal − esquema.totalPagado (lo que queda para financiar) */ saldoAFinanciar: number /** SEMI: saldoAFinanciar * pctContado; resto 0 */ pagoContado: number /** saldoAFinanciar − pagoContado (0 en CONTADO) */ montoCredito: number tasaAnual: number plazoAnos: number pctContado?: number /** Anualidad del credito; null en CONTADO */ mensualidadEstimada: number | null /** mensualidad * 12 * plazoAnos − montoCredito (0 en CONTADO) */ interesesEstimados: number /** precioFinal + interesesEstimados */ totalEstimado: number /** === montoDescuento: lo que se ahorra vs el precio de lista */ ahorroVsLista: number /** false si el apartado excede el 10% del precio final (enganche negativo) */ esquemaVigente: boolean } /** Error de validación de ajustes; null si son válidos */ export function validarAjustes(tipo: TipoCredito, a: AjustesCredito): string | null { if (!Number.isFinite(a.tasaAnual) || a.tasaAnual < 0 || a.tasaAnual > 0.5) { return 'La tasa anual debe estar entre 0% y 50%' } if (!Number.isInteger(a.plazoAnos) || a.plazoAnos < 1 || a.plazoAnos > 35) { return 'El plazo debe ser un número entero de 1 a 35 años' } if (tipo === 'SEMI_CONTADO' && a.pctContado !== undefined) { if (!Number.isFinite(a.pctContado) || a.pctContado < 0 || a.pctContado > 1) { return 'El porcentaje de contado debe estar entre 0% y 100%' } } return null } /** Anualidad (mensualidad) de un crédito: monto * i / (1 − (1+i)^−n) */ function mensualidadCredito(monto: number, tasaAnual: number, plazoAnos: number): number { const n = plazoAnos * 12 if (tasaAnual === 0) return monto / n const i = tasaAnual / 12 return (monto * i) / (1 - Math.pow(1 + i, -n)) } export function calcularOpcionCredito(e: EntradaOpcion): ResultadoOpcionCredito { const errorAjustes = validarAjustes(e.tipo, e.ajustes) if (errorAjustes) throw new Error(errorAjustes) const descuento = DESCUENTO_POR_TIPO[e.tipo] const montoDescuento = round2(e.precioLista * descuento) const precioFinal = e.precioLista - montoDescuento const esquemaBase = calcularEsquemaFrasa({ precio: precioFinal, apartado: e.apartado, numMensualidades: e.numMensualidades, escrituracionDiferida: e.escrituracionDiferida, }) // El adicional se acota igual que en el hook: el anticipo total no puede // superar el 90% del precio final de la opción const adicionalSolicitado = Math.max(0, e.engancheAdicional ?? 0) const adicionalEfectivo = Math.min(adicionalSolicitado, round2(precioFinal * PCT_ENGANCHE_MAXIMO - esquemaBase.totalPagado)) const esquema = adicionalEfectivo > 0 ? calcularEsquemaFrasa({ precio: precioFinal, apartado: e.apartado, numMensualidades: e.numMensualidades, escrituracionDiferida: e.escrituracionDiferida, engancheAdicional: adicionalEfectivo, }) : esquemaBase const saldoAFinanciar = round2(esquema.saldoAFinanciar) const esContado = e.tipo === 'CONTADO' const pctContado = e.tipo === 'SEMI_CONTADO' ? (e.ajustes.pctContado ?? 0) : 0 const pagoContado = esContado ? 0 : round2(saldoAFinanciar * pctContado) const montoCredito = esContado ? 0 : round2(saldoAFinanciar - pagoContado) const mensualidadEstimada = esContado ? null : round2(mensualidadCredito(montoCredito, e.ajustes.tasaAnual, e.ajustes.plazoAnos)) const interesesEstimados = esContado ? 0 : round2(mensualidadEstimada! * 12 * e.ajustes.plazoAnos - montoCredito) const totalEstimado = round2(precioFinal + interesesEstimados) return { tipo: e.tipo, titulo: TITULO_POR_TIPO[e.tipo], descuento, montoDescuento, precioFinal, esquema, saldoAFinanciar, pagoContado, montoCredito, tasaAnual: e.ajustes.tasaAnual, plazoAnos: e.ajustes.plazoAnos, ...(e.tipo === 'SEMI_CONTADO' ? { pctContado: e.ajustes.pctContado } : {}), mensualidadEstimada, interesesEstimados, totalEstimado, ahorroVsLista: montoDescuento, esquemaVigente: esquema.enganche >= 0, } }