import { useMemo, useState } from 'react' import type { Unidad, ParametrosDesarrollo } from '../types' import { obtenerPrecioLista, resolverNumMensualidades } from '../lib/calculations' import { DESARROLLO_DEFAULT } from '../data/desarrollos' import { MENSUALIDADES_DEFAULT, MENSUALIDADES_MAX, MENSUALIDADES_MIN, PCT_ENGANCHE_MAXIMO, calcularEsquemaFrasa, type EsquemaFrasaResultado, } from '../lib/esquema-frasa' import { SIN_REGLAS, desarrolloEfectivo, type ReglasDesarrollo } from '../lib/reglas' /** Apartado con el que arranca (y se reinicia) el cotizador: cantidad libre */ export const APARTADO_ESQUEMA_DEFAULT = 15000 const MS_POR_MES = 30.4 * 86400000 /** * Tope (y default) de mensualidades del diferimiento para una unidad: los * meses que faltan hasta su fecha de entrega estimada (la que vive en el * módulo de captura: entrega programada/reprogramada de obra). El diferimiento * nunca puede rebasar la entrega de la vivienda. Si la unidad no trae fecha, * cae al default global del esquema (14; tope 36). */ export function mensualidadesParaUnidad(u: Unidad | null, hoy: Date = new Date()): number { const fecha = u?.fechaEntrega if (!fecha) return MENSUALIDADES_DEFAULT const entrega = new Date(`${fecha}T00:00:00`).getTime() if (!Number.isFinite(entrega)) return MENSUALIDADES_DEFAULT const sugeridas = Math.max(MENSUALIDADES_MIN, Math.round((entrega - hoy.getTime()) / MS_POR_MES)) return Math.min(MENSUALIDADES_MAX, sugeridas) } interface UseCotizacionResult { desarrollo: string /** Cambia de desarrollo: limpia la unidad elegida */ setDesarrollo: (id: string) => void params: ParametrosDesarrollo unidad: Unidad | null setUnidad: (u: Unidad | null) => void acabados: boolean setAcabados: (v: boolean) => void apartado: number setApartado: (v: number) => void /** Apartado aplicado a los cálculos (acotado a >= 0; el vendedor puede apartar con cualquier monto, incluso $0) */ apartadoEfectivo: number /** Número de mensualidades del diferimiento (1..36): al elegir unidad se * prellena con los meses que faltan para su entrega estimada; el vendedor * puede ajustarla */ numMensualidades: number setNumMensualidades: (n: number) => void /** true = la escrituración (5.5%) se difiere con el enganche; false = se paga a la firma */ escrituracionDiferida: boolean setEscrituracionDiferida: (v: boolean) => void /** Enganche adicional (anticipo extra, opcional): el total de anticipos no * puede superar el 90% del precio (mínimo 10% queda por financiar) */ engancheAdicional: number setEngancheAdicional: (v: number) => void /** Máximo de enganche adicional permitido por el tope del 90% (0 sin unidad) */ engancheAdicionalMaximo: number precioLista: number /** Esquema FRASA 2026 calculado; null mientras no hay unidad elegida */ resultado: EsquemaFrasaResultado | null /** Meses restantes para la entrega de la unidad (ficha técnica); null si no se pueden resolver */ numMeses: number | null /** Restaura el estado completo desde una cotización guardada (historial) */ restaurar: (estado: EstadoRestaurar) => void /** Reinicia la cotización (botón "Nueva") */ nueva: () => void } export interface EstadoRestaurar { desarrollo: string /** Unidad ya resuelta del inventario cargado */ unidad: Unidad acabados: boolean apartado: number /** Mensualidades del diferimiento guardadas (default 14) */ numMensualidades?: number /** Escrituración diferida guardada (default false) */ escrituracionDiferida?: boolean /** Enganche adicional guardado (default 0) */ engancheAdicional?: number } /** * Estado central de la cotización con el esquema único FRASA 2026: * desarrollo, unidad, acabados, apartado libre, mensualidades del * diferimiento (1..36, default 14) y si la escrituración se difiere o se paga * a la firma. El resultado sale de calcularEsquemaFrasa (módulo puro, mismo * motor que el API). Ya no hay propuestas: descuentos, enganche en % e índice * de propuesta desaparecieron con las 4 propuestas. */ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizacionResult { const [desarrollo, setDesarrolloState] = useState(DESARROLLO_DEFAULT) const [unidad, setUnidad] = useState(null) const [acabados, setAcabados] = useState(false) const [apartado, setApartado] = useState(APARTADO_ESQUEMA_DEFAULT) const [numMensualidades, setNumMensualidades] = useState(MENSUALIDADES_DEFAULT) const [escrituracionDiferida, setEscrituracionDiferida] = useState(false) const [engancheAdicional, setEngancheAdicionalState] = useState(0) const params = desarrolloEfectivo(reglas, desarrollo) const setDesarrollo = (id: string) => { setDesarrolloState(id) setUnidad(null) setAcabados(false) } /** Elige unidad y, con ella, las mensualidades del diferimiento: los meses * que faltan para la entrega estimada de esa vivienda (dato del módulo de * captura). El vendedor puede ajustarlas después, sin rebasar la entrega. */ const elegirUnidad = (u: Unidad | null) => { setUnidad(u) setNumMensualidades(mensualidadesParaUnidad(u)) } /** Tope de mensualidades: las que faltan para la entrega (36 sin fecha) */ const topeMensualidades = () => mensualidadesParaUnidad(unidad) const ajustarMensualidades = (n: number) => { setNumMensualidades(Math.max(MENSUALIDADES_MIN, Math.min(n, topeMensualidades()))) } /** Enganche adicional: acotado a >= 0; el esquema valida que el anticipo * total no supere el 90% del precio (mínimo 10% por financiar) */ const setEngancheAdicional = (v: number) => { setEngancheAdicionalState(Number.isFinite(v) ? Math.max(0, v) : 0) } const restaurar = (estado: EstadoRestaurar) => { setDesarrolloState(estado.desarrollo) setUnidad(estado.unidad) setAcabados(estado.acabados) setApartado(estado.apartado) // El diferimiento no puede rebasar la entrega: una cotización guardada con // más mensualidades se acota a las que faltan hoy para esa unidad setNumMensualidades( Math.min(estado.numMensualidades ?? MENSUALIDADES_DEFAULT, mensualidadesParaUnidad(estado.unidad)), ) setEscrituracionDiferida(estado.escrituracionDiferida ?? false) setEngancheAdicionalState(Math.max(0, estado.engancheAdicional ?? 0)) } const nueva = () => { setUnidad(null) setAcabados(false) setApartado(APARTADO_ESQUEMA_DEFAULT) setNumMensualidades(MENSUALIDADES_DEFAULT) setEscrituracionDiferida(false) setEngancheAdicionalState(0) } const numMeses = useMemo( () => (unidad ? resolverNumMensualidades(unidad, params) : null), [unidad, params], ) const precioLista = useMemo( () => (unidad ? obtenerPrecioLista(unidad, acabados) : 0), [unidad, acabados], ) // Sin mínimo de apartado: el vendedor puede apartar con cualquier monto, // incluso $0; solo se acota a >= 0 como guarda contra capturas raras const apartadoEfectivo = Math.max(0, apartado) /** * Esquema FRASA 2026 calculado; null mientras no hay unidad elegida. El * enganche adicional se acota al máximo que permite el 90% de anticipo * (el esquema lanza error si el total lo rebasa; aquí se previene). */ const resultado = useMemo(() => { if (!unidad || precioLista <= 0) return null const base = calcularEsquemaFrasa({ precio: precioLista, apartado: apartadoEfectivo, numMensualidades, escrituracionDiferida, }) const maxAdicional = Math.max(0, precioLista * PCT_ENGANCHE_MAXIMO - base.totalPagado) const adicionalEfectivo = Math.min(Math.max(0, engancheAdicional), maxAdicional) if (adicionalEfectivo === 0) return base return calcularEsquemaFrasa({ precio: precioLista, apartado: apartadoEfectivo, numMensualidades, escrituracionDiferida, engancheAdicional: adicionalEfectivo, }) }, [unidad, precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida, engancheAdicional]) /** Máximo de enganche adicional que permite el 90% de anticipo (0 sin unidad) */ const engancheAdicionalMaximo = useMemo(() => { if (!resultado) return 0 return Math.max(0, precioLista * PCT_ENGANCHE_MAXIMO - (resultado.totalPagado - resultado.engancheAdicional)) }, [resultado, precioLista]) return { desarrollo, setDesarrollo, params, unidad, setUnidad: elegirUnidad, acabados, setAcabados, apartado, setApartado, apartadoEfectivo, numMensualidades, setNumMensualidades: ajustarMensualidades, escrituracionDiferida, setEscrituracionDiferida, engancheAdicional, setEngancheAdicional, engancheAdicionalMaximo, precioLista, resultado, numMeses, restaurar, nueva, } }