227 lines
8.7 KiB
TypeScript
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,
|
|
}
|
|
}
|