// Lógica financiera para calculadora de PREVENTA (esquema en porcentajes) const fmtMXN = (n, opts = {}) => { if (n === null || n === undefined || isNaN(n)) return "—"; const { decimals = 0, compact = false } = opts; if (compact && Math.abs(n) >= 1_000_000) { return "$" + (n / 1_000_000).toLocaleString("es-MX", { maximumFractionDigits: 2 }) + " M"; } if (compact && Math.abs(n) >= 1_000) { return "$" + (n / 1_000).toLocaleString("es-MX", { maximumFractionDigits: 1 }) + " K"; } return "$" + n.toLocaleString("es-MX", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); }; const fmtPct = (n, decimals = 2) => { if (n === null || n === undefined || isNaN(n) || !isFinite(n)) return "—"; return n.toLocaleString("es-MX", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + "%"; }; const fmtNum = (n, decimals = 0) => { if (n === null || n === undefined || isNaN(n)) return "—"; return n.toLocaleString("es-MX", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); }; // Pago hipotecario sistema francés function pagoHip(monto, tasaAnual, plazoAnios) { if (monto <= 0 || plazoAnios <= 0) return 0; const n = plazoAnios * 12; const r = tasaAnual / 100 / 12; if (r === 0) return monto / n; return (monto * r * Math.pow(1 + r, n)) / (Math.pow(1 + r, n) - 1); } function saldoCreditoEnAnio(monto, tasaAnual, plazoAnios, anio) { if (monto <= 0 || anio <= 0) return monto; if (anio >= plazoAnios) return 0; const r = tasaAnual / 100 / 12; const n = plazoAnios * 12; const m = anio * 12; if (r === 0) return monto * (1 - m / n); const pago = pagoHip(monto, tasaAnual, plazoAnios); // Saldo = pago * ((1 - (1+r)^(-(n-m))) / r) return pago * (1 - Math.pow(1 + r, -(n - m))) / r; } // Cálculo principal del esquema de pagos function calcular(i) { const precio = +i.precio || 0; const engPct = +i.engPct || 0; const mensPct = +i.mensPct || 0; const entregaPct = +i.entregaPct || 0; const escrituraPct = +i.escrituraPct || 0; const numMensualidades = Math.max(1, Math.floor(+i.numMensualidades || 1)); const sumaEsquema = engPct + mensPct + entregaPct; const balanceadoOk = Math.abs(sumaEsquema - 100) < 0.01; const enganche = precio * (engPct / 100); const totalMensualidades = precio * (mensPct / 100); const mensualidad = numMensualidades > 0 ? totalMensualidades / numMensualidades : 0; const contraEntrega = precio * (entregaPct / 100); const escrituracion = precio * (escrituraPct / 100); const totalAPagar = enganche + totalMensualidades + contraEntrega + escrituracion; const totalSinEscritura = enganche + totalMensualidades + contraEntrega; // Cronograma const cronograma = []; cronograma.push({ mes: 0, concepto: "Firma de contrato", detalle: "Enganche", pago: enganche, porcentaje: engPct }); for (let m = 1; m <= numMensualidades; m++) { cronograma.push({ mes: m, concepto: `Mensualidad ${m} de ${numMensualidades}`, detalle: "Pago durante construcción", pago: mensualidad, porcentaje: numMensualidades > 0 ? mensPct / numMensualidades : 0, }); } cronograma.push({ mes: numMensualidades + 1, concepto: "Entrega", detalle: "Saldo contra entrega", pago: contraEntrega, porcentaje: entregaPct }); cronograma.push({ mes: numMensualidades + 1, concepto: "Escrituración", detalle: "Gastos notariales (no cuenta para esquema)", pago: escrituracion, porcentaje: escrituraPct, extra: true }); let acum = 0; cronograma.forEach((p) => { acum += p.pago; p.acumulado = acum; p.porcentajeAcum = precio > 0 ? (acum / precio) * 100 : 0; }); return { precio, enganche, totalMensualidades, mensualidad, contraEntrega, escrituracion, totalAPagar, totalSinEscritura, sumaEsquema, balanceadoOk, numMensualidades, mensPct, engPct, entregaPct, escrituraPct, cronograma, }; } // Cálculo del ROI post-entrega function calcularROI(precio, contraEntrega, escrituracion, totalAPagar, roiInputs) { const tarifaProm = +roiInputs.tarifaProm || 0; const ocupacion = +roiInputs.ocupacion || 0; const comisionPct = +roiInputs.comisionPct || 0; const adminPct = +roiInputs.adminPct || 0; const gastosFijos = +roiInputs.gastosFijos || 0; const plusvalia = +roiInputs.plusvalia || 0; const inflacionRenta = +roiInputs.inflacionRenta || 0; const horizonte = Math.max(1, Math.floor(+roiInputs.horizonte || 10)); // Crédito sobre el saldo contra entrega const usarCredito = !!roiInputs.usarCredito; const tasaCredito = +roiInputs.tasaCredito || 0; const plazoCredito = Math.max(1, Math.floor(+roiInputs.plazoCredito || 20)); const montoCredito = usarCredito ? contraEntrega : 0; const pagoMensualHip = usarCredito ? pagoHip(montoCredito, tasaCredito, plazoCredito) : 0; // Inversión propia: lo que el cliente pone de su bolsa. // Si usa crédito, el banco aporta el contra entrega; el cliente sólo aporta enganche + mensualidades + escrituración. const inversionPropia = usarCredito ? totalAPagar - contraEntrega : totalAPagar; // Ingreso mensual base const noches = 30 * (ocupacion / 100); const ingresoBruto = tarifaProm * noches; const comision = ingresoBruto * (comisionPct / 100); const admin = ingresoBruto * (adminPct / 100); const ingresoNetoOp = ingresoBruto - comision - admin - gastosFijos; const flujoNetoMensual = ingresoNetoOp - pagoMensualHip; const flujoNetoAnual = flujoNetoMensual * 12; // Indicador "se paga solo": % de la hipoteca cubierta por la renta neta operativa const cubrePctHip = pagoMensualHip > 0 ? (ingresoNetoOp / pagoMensualHip) * 100 : null; // Proyección año a año const proyeccion = []; let valor = precio; let renta = ingresoNetoOp; let acumFlujo = 0; for (let y = 1; y <= horizonte; y++) { const flujoOpAnual = renta * 12; const pagoHipAnual = pagoMensualHip * 12; const flujoAnioY = flujoOpAnual - pagoHipAnual; acumFlujo += flujoAnioY; valor = valor * (1 + plusvalia / 100); const saldo = usarCredito ? saldoCreditoEnAnio(montoCredito, tasaCredito, plazoCredito, y) : 0; const equity = valor - saldo; proyeccion.push({ anio: y, ingresoOp: flujoOpAnual, pagoHip: pagoHipAnual, flujoNeto: flujoAnioY, flujoAcumulado: acumFlujo, valor, saldo, equity, plusvaliaAcum: valor - precio, }); renta = renta * (1 + inflacionRenta / 100); } const ultimo = proyeccion[proyeccion.length - 1]; const plusvaliaTotal = ultimo.valor - precio; const flujoAcumTotal = ultimo.flujoAcumulado; const equityFinal = ultimo.equity; // Ganancia total = equity al final + flujo acumulado − inversión propia const gananciaTotal = equityFinal + flujoAcumTotal - inversionPropia; const roiTotal = inversionPropia > 0 ? (gananciaTotal / inversionPropia) * 100 : 0; const roiAnualizado = inversionPropia > 0 && horizonte > 0 ? (Math.pow(1 + roiTotal / 100, 1 / horizonte) - 1) * 100 : 0; const capRate = precio > 0 ? ((ingresoNetoOp * 12) / precio) * 100 : 0; const cashOnCash = inversionPropia > 0 ? (flujoNetoAnual / inversionPropia) * 100 : 0; return { tarifaProm, ocupacion, noches, ingresoBruto, comision, admin, gastosFijos, ingresoNetoOp, pagoMensualHip, flujoNetoMensual, flujoNetoAnual, cubrePctHip, usarCredito, montoCredito, tasaCredito, plazoCredito, inversionPropia, proyeccion, plusvaliaTotal, flujoAcumTotal, equityFinal, gananciaTotal, roiTotal, roiAnualizado, capRate, cashOnCash, horizonte, plusvalia, inflacionRenta, // Sin apalancamiento: como si el cliente pagara todo de su bolsa ...(function() { const inversionSinApal = totalAPagar; // todo el desembolso const flujoOpAnualSinHip = ingresoNetoOp * 12; // sin hipoteca // proyeccion sin hipoteca let acum = 0; let renta = ingresoNetoOp; for (let y = 1; y <= horizonte; y++) { acum += renta * 12; renta = renta * (1 + inflacionRenta / 100); } const valorFinal = precio * Math.pow(1 + plusvalia / 100, horizonte); const gananciaSinApal = (valorFinal - precio) + acum; const roiSinApal = inversionSinApal > 0 ? (gananciaSinApal / inversionSinApal) * 100 : 0; const roiAnualSinApal = inversionSinApal > 0 && horizonte > 0 ? (Math.pow(1 + roiSinApal / 100, 1 / horizonte) - 1) * 100 : 0; const cashOnCashSinApal = inversionSinApal > 0 ? (flujoOpAnualSinHip / inversionSinApal) * 100 : 0; return { inversionSinApal, roiSinApal, roiAnualSinApal, cashOnCashSinApal, flujoMensualSinApal: ingresoNetoOp }; })(), }; } window.CalcLogic = { calcular, calcularROI, fmtMXN, fmtPct, fmtNum };