Files
FRASA-CAS/apps/cotizador/src/hooks/useCotizacion.ts

227 lines
8.7 KiB
TypeScript

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<string>(DESARROLLO_DEFAULT)
const [unidad, setUnidad] = useState<Unidad | null>(null)
const [acabados, setAcabados] = useState(false)
const [apartado, setApartado] = useState<number>(APARTADO_ESQUEMA_DEFAULT)
const [numMensualidades, setNumMensualidades] = useState<number>(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<EsquemaFrasaResultado | null>(() => {
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,
}
}