feat(cotizador): enganche adicional hasta 90% de anticipo total (UI, opciones de credito, PDF, API y persistencia)

This commit is contained in:
2026-09-29 21:08:02 +00:00
parent ed367582c3
commit 013e13b9df
15 changed files with 309 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ import {
MENSUALIDADES_DEFAULT,
MENSUALIDADES_MAX,
MENSUALIDADES_MIN,
PCT_ENGANCHE_MAXIMO,
calcularEsquemaFrasa,
type EsquemaFrasaResultado,
} from '../lib/esquema-frasa'
@@ -53,6 +54,12 @@ interface UseCotizacionResult {
/** 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
@@ -74,6 +81,8 @@ export interface EstadoRestaurar {
numMensualidades?: number
/** Escrituración diferida guardada (default false) */
escrituracionDiferida?: boolean
/** Enganche adicional guardado (default 0) */
engancheAdicional?: number
}
/**
@@ -91,6 +100,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
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)
@@ -115,6 +125,12 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
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)
@@ -126,6 +142,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
Math.min(estado.numMensualidades ?? MENSUALIDADES_DEFAULT, mensualidadesParaUnidad(estado.unidad)),
)
setEscrituracionDiferida(estado.escrituracionDiferida ?? false)
setEngancheAdicionalState(Math.max(0, estado.engancheAdicional ?? 0))
}
const nueva = () => {
@@ -134,6 +151,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
setApartado(APARTADO_ESQUEMA_DEFAULT)
setNumMensualidades(MENSUALIDADES_DEFAULT)
setEscrituracionDiferida(false)
setEngancheAdicionalState(0)
}
const numMeses = useMemo(
@@ -150,15 +168,36 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
// 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
return calcularEsquemaFrasa({
const base = calcularEsquemaFrasa({
precio: precioLista,
apartado: apartadoEfectivo,
numMensualidades,
escrituracionDiferida,
})
}, [unidad, precioLista, 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,
@@ -175,6 +214,9 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
setNumMensualidades: ajustarMensualidades,
escrituracionDiferida,
setEscrituracionDiferida,
engancheAdicional,
setEngancheAdicional,
engancheAdicionalMaximo,
precioLista,
resultado,
numMeses,