feat: esquema unico de cotizacion FRASA 2026 (reemplaza las 4 propuestas)
Flujo aprobado por el cliente (28 sep 2026), porcentajes globales para todos los desarrollos: - Apartado: cantidad libre (sin minimo), se acredita dentro del enganche de firma; el excedente se descuenta del enganche - Enganche de firma: 2.5% del precio (saldo a firma = max(0, 2.5% - apartado)) - Enganche: 7.5%, diferible en N mensualidades (N a eleccion del vendedor, 1-36) - Gastos de escrituracion: 5.5%, pago unico a la firma o diferido con el enganche (toggle) - lib/esquema-frasa.ts (frontend) + cotizacion-esquema.service.ts (backend): modulos puros espejo, caso dorado M/apartado 5k/N=14 en tests de ambos - Migracion 20260928120000: 7 columnas nuevas en Cotizacion (esquema, saldoFirma, engancheDiferible, escrituracion, etc.); legacy se rellena para compatibilidad de reportes (tipoPropuesta=FRASA_2026) - UI: panel de linea de tiempo (Hoy -> Firma -> Mensualidades) sustituye a propuestas/comparadores; PDF y WhatsApp con el esquema unico - API recalcula server-side; smoke end-to-end verificado contra inventario - 743 tests API + 128 cotizador en verde
This commit is contained in:
@@ -1,31 +1,22 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { act, cleanup, renderHook } from '@testing-library/react'
|
||||
import { useCotizacion } from './useCotizacion'
|
||||
import { reglasDesdeApi, SIN_REGLAS, type ReglasDesarrollo } from '../lib/reglas'
|
||||
import { DESARROLLOS } from '../data/desarrollos'
|
||||
import { APARTADO_ESQUEMA_DEFAULT, useCotizacion } from './useCotizacion'
|
||||
import { SIN_REGLAS } from '../lib/reglas'
|
||||
import { MENSUALIDADES_DEFAULT, calcularEsquemaFrasa } from '../lib/esquema-frasa'
|
||||
import { INVENTARIO_SEED } from '../data/unidades'
|
||||
|
||||
/**
|
||||
* Reglas comerciales configurables en el cotizador: las propuestas desactivadas
|
||||
* por el admin dejan de ofrecerse y, si la elegida se desactiva, se avisa y se
|
||||
* cae a la primera vigente.
|
||||
* Estado central con el esquema único FRASA 2026: apartado libre (default
|
||||
* $15,000), mensualidades del diferimiento (default 14) y escrituración a la
|
||||
* firma o diferida. Ya no hay propuestas: el resultado sale de
|
||||
* calcularEsquemaFrasa (mismo motor que el API).
|
||||
*/
|
||||
|
||||
const unidadDiagonal = INVENTARIO_SEED.find((u) => u.desarrollo === 'DIAGONAL')!
|
||||
|
||||
function reglasDiagonales(activa: (tipo: string) => boolean): ReglasDesarrollo {
|
||||
return reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) => ({ ...p, activa: activa(p.tipo) })),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function montar(reglas: ReglasDesarrollo = SIN_REGLAS) {
|
||||
const hook = renderHook(({ reglas }) => useCotizacion(reglas), {
|
||||
initialProps: { reglas: reglas as ReglasDesarrollo },
|
||||
})
|
||||
function montar() {
|
||||
const hook = renderHook(() => useCotizacion(SIN_REGLAS))
|
||||
act(() => {
|
||||
hook.result.current.setUnidad(unidadDiagonal)
|
||||
})
|
||||
@@ -34,75 +25,128 @@ function montar(reglas: ReglasDesarrollo = SIN_REGLAS) {
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('useCotizacion y las reglas del admin', () => {
|
||||
it('sin reglas ofrece las 4 propuestas de siempre', () => {
|
||||
describe('useCotizacion — esquema único FRASA 2026', () => {
|
||||
it('arranca con apartado $15,000, 14 mensualidades y escrituración a la firma', () => {
|
||||
const { result } = montar()
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).toEqual([
|
||||
'CREDITO_BANCARIO',
|
||||
'CREDITO_INFONAVIT_COFINAVIT',
|
||||
'CONTADO',
|
||||
'SEMI_CONTADO',
|
||||
])
|
||||
expect(result.current.avisoPropuestaDesactivada).toBeNull()
|
||||
expect(result.current.apartado).toBe(APARTADO_ESQUEMA_DEFAULT)
|
||||
expect(result.current.apartadoEfectivo).toBe(15000)
|
||||
expect(result.current.numMensualidades).toBe(MENSUALIDADES_DEFAULT)
|
||||
expect(result.current.escrituracionDiferida).toBe(false)
|
||||
})
|
||||
|
||||
it('una propuesta desactivada no se ofrece ni se puede elegir', () => {
|
||||
const { result } = montar(reglasDiagonales((tipo) => tipo !== 'CONTADO'))
|
||||
it('el resultado cuadra con calcularEsquemaFrasa sobre el precio de lista', () => {
|
||||
const { result } = montar()
|
||||
const { unidad, acabados } = result.current
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).toEqual([
|
||||
'CREDITO_BANCARIO',
|
||||
'CREDITO_INFONAVIT_COFINAVIT',
|
||||
'SEMI_CONTADO',
|
||||
])
|
||||
})
|
||||
|
||||
it('si el admin desactiva la propuesta elegida, avisa y cae a la primera vigente', () => {
|
||||
const { result, rerender } = montar()
|
||||
|
||||
// El vendedor elige Contado (índice 2)
|
||||
act(() => {
|
||||
result.current.setPropuestaIndex(2)
|
||||
const esperado = calcularEsquemaFrasa({
|
||||
precio: result.current.precioLista,
|
||||
apartado: 15000,
|
||||
numMensualidades: 14,
|
||||
escrituracionDiferida: false,
|
||||
})
|
||||
expect(result.current.propuestas[2]?.tipo).toBe('CONTADO')
|
||||
|
||||
// El admin la desactiva (llegan reglas nuevas)
|
||||
rerender({ reglas: reglasDiagonales((tipo) => tipo !== 'CONTADO') })
|
||||
|
||||
expect(result.current.propuestas.map((p) => p.tipo)).not.toContain('CONTADO')
|
||||
expect(result.current.propuestaIndex).toBe(0)
|
||||
expect(result.current.avisoPropuestaDesactivada).toContain('Contado')
|
||||
expect(result.current.avisoPropuestaDesactivada).toContain('Crédito Bancario')
|
||||
expect(result.current.resultado).toEqual(esperado)
|
||||
// El esquema siempre liquida el 15.5% del precio (2.5 + 7.5 + 5.5)
|
||||
expect(result.current.resultado?.totalPagado).toBeCloseTo(result.current.precioLista * 0.155, 2)
|
||||
expect(unidad).toBe(unidadDiagonal)
|
||||
expect(acabados).toBe(false)
|
||||
})
|
||||
|
||||
it('los descuentos por defecto salen de las reglas configuradas', () => {
|
||||
const { result } = montar(
|
||||
reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) =>
|
||||
p.tipo === 'CONTADO' ? { ...p, descuentoDefault: 0.06 } : p,
|
||||
),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(result.current.descuentos.contado).toBe(0.06)
|
||||
expect(result.current.descuentos.semiContado).toBe(0.01)
|
||||
it('sin unidad no hay resultado (panel vacío)', () => {
|
||||
const { result } = montar()
|
||||
act(() => {
|
||||
result.current.setUnidad(null)
|
||||
})
|
||||
expect(result.current.resultado).toBeNull()
|
||||
expect(result.current.precioLista).toBe(0)
|
||||
})
|
||||
|
||||
it('las reglas cambian los números de las propuestas', () => {
|
||||
const { result } = montar(
|
||||
reglasDesdeApi({
|
||||
DIAGONAL: {
|
||||
propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) =>
|
||||
p.tipo === 'CREDITO_BANCARIO' ? { ...p, pctEnganche: 0.05 } : p,
|
||||
),
|
||||
},
|
||||
}),
|
||||
)
|
||||
it('el apartado libre alimenta el saldo a firma; el excedente acredita al enganche', () => {
|
||||
const { result } = montar()
|
||||
const excedente = Math.round(result.current.precioLista * 0.025) + 5000
|
||||
|
||||
const bancario = result.current.propuestas.find((p) => p.tipo === 'CREDITO_BANCARIO')
|
||||
const precioLista = result.current.precioLista
|
||||
expect(bancario?.enganche).toBeCloseTo(0.05 * precioLista - result.current.apartadoEfectivo, 2)
|
||||
act(() => {
|
||||
result.current.setApartado(excedente)
|
||||
})
|
||||
const r = result.current.resultado!
|
||||
expect(r.excedenteApartado).toBeGreaterThan(0)
|
||||
expect(r.saldoFirma).toBe(0)
|
||||
// El total no cambia: el excedente se mueve de concepto, no se cobra dos veces
|
||||
expect(r.totalPagado).toBeCloseTo(result.current.precioLista * 0.155, 2)
|
||||
})
|
||||
|
||||
it('las mensualidades elegidas recalculan la mensualidad sobre el diferible', () => {
|
||||
const { result } = montar()
|
||||
|
||||
act(() => {
|
||||
result.current.setNumMensualidades(12)
|
||||
})
|
||||
const r = result.current.resultado!
|
||||
expect(r.numMensualidades).toBe(12)
|
||||
expect(r.mensualidad).toBeCloseTo(r.baseDiferible / 12, 6)
|
||||
})
|
||||
|
||||
it('escrituración diferida: sube el diferible y baja el pago a la firma', () => {
|
||||
const { result } = montar()
|
||||
|
||||
act(() => {
|
||||
result.current.setEscrituracionDiferida(true)
|
||||
})
|
||||
const r = result.current.resultado!
|
||||
expect(r.escrituracionDiferida).toBe(true)
|
||||
expect(r.baseDiferible).toBeCloseTo(r.enganche + r.escrituracion, 6)
|
||||
expect(r.pagoUnicoFirma).toBeCloseTo(r.saldoFirma, 6)
|
||||
})
|
||||
|
||||
it('restaurar carga una cotización guardada del esquema (N y escrituración)', () => {
|
||||
const { result } = montar()
|
||||
|
||||
act(() => {
|
||||
result.current.restaurar({
|
||||
desarrollo: 'DIAGONAL',
|
||||
unidad: unidadDiagonal,
|
||||
acabados: true,
|
||||
apartado: 20000,
|
||||
numMensualidades: 24,
|
||||
escrituracionDiferida: true,
|
||||
})
|
||||
})
|
||||
expect(result.current.acabados).toBe(true)
|
||||
expect(result.current.apartadoEfectivo).toBe(20000)
|
||||
expect(result.current.numMensualidades).toBe(24)
|
||||
expect(result.current.escrituracionDiferida).toBe(true)
|
||||
expect(result.current.resultado?.numMensualidades).toBe(24)
|
||||
})
|
||||
|
||||
it('restaurar una cotización legacy (sin N ni escrituración) cae a los defaults', () => {
|
||||
const { result } = montar()
|
||||
|
||||
act(() => {
|
||||
result.current.restaurar({
|
||||
desarrollo: 'DIAGONAL',
|
||||
unidad: unidadDiagonal,
|
||||
acabados: false,
|
||||
apartado: 20000,
|
||||
})
|
||||
})
|
||||
expect(result.current.numMensualidades).toBe(MENSUALIDADES_DEFAULT)
|
||||
expect(result.current.escrituracionDiferida).toBe(false)
|
||||
})
|
||||
|
||||
it('nueva reinicia apartado, mensualidades y escrituración, y limpia la unidad', () => {
|
||||
const { result } = montar()
|
||||
|
||||
act(() => {
|
||||
result.current.setApartado(40000)
|
||||
result.current.setNumMensualidades(6)
|
||||
result.current.setEscrituracionDiferida(true)
|
||||
})
|
||||
act(() => {
|
||||
result.current.nueva()
|
||||
})
|
||||
expect(result.current.unidad).toBeNull()
|
||||
expect(result.current.apartado).toBe(APARTADO_ESQUEMA_DEFAULT)
|
||||
expect(result.current.numMensualidades).toBe(MENSUALIDADES_DEFAULT)
|
||||
expect(result.current.escrituracionDiferida).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Unidad, DescuentosPropuestas, ParametrosDesarrollo } from '../types'
|
||||
import {
|
||||
APARTADO_DEFAULT,
|
||||
calcularPropuestas,
|
||||
obtenerPrecioLista,
|
||||
resolverNumMensualidades,
|
||||
validarEnganchePct,
|
||||
type PropuestaResultado,
|
||||
} from '../lib/calculations'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { Unidad, ParametrosDesarrollo } from '../types'
|
||||
import { obtenerPrecioLista, resolverNumMensualidades } from '../lib/calculations'
|
||||
import { DESARROLLO_DEFAULT } from '../data/desarrollos'
|
||||
import {
|
||||
SIN_REGLAS,
|
||||
descuentosDefaultEfectivo,
|
||||
desarrolloEfectivo,
|
||||
propuestasVigentes,
|
||||
type ReglasDesarrollo,
|
||||
} from '../lib/reglas'
|
||||
MENSUALIDADES_DEFAULT,
|
||||
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
|
||||
|
||||
interface UseCotizacionResult {
|
||||
desarrollo: string
|
||||
/** Cambia de desarrollo: limpia la unidad elegida y reinicia los descuentos */
|
||||
/** Cambia de desarrollo: limpia la unidad elegida */
|
||||
setDesarrollo: (id: string) => void
|
||||
params: ParametrosDesarrollo
|
||||
unidad: Unidad | null
|
||||
@@ -30,25 +25,17 @@ interface UseCotizacionResult {
|
||||
setApartado: (v: number) => void
|
||||
/** Apartado aplicado a los cálculos (acotado a >= 0; el vendedor puede apartar con cualquier monto, incluso $0) */
|
||||
apartadoEfectivo: number
|
||||
/** Enganche capturado en PORCENTAJE (10 = 10%); null = % del Excel de cada propuesta */
|
||||
enganchePct: number | null
|
||||
setEnganchePct: (v: number | null) => void
|
||||
/** Mensaje de error del enganche capturado; null si es válido */
|
||||
errorEnganche: string | null
|
||||
descuentos: DescuentosPropuestas
|
||||
setDescuento: (clave: keyof DescuentosPropuestas, valor: number) => void
|
||||
/** Índice de la propuesta elegida (para guardar/compartir) */
|
||||
propuestaIndex: number
|
||||
setPropuestaIndex: (i: number) => void
|
||||
/** Número de mensualidades del diferimiento (elección del vendedor, 1..36) */
|
||||
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
|
||||
precioLista: number
|
||||
/** Número de mensualidades elegido por el vendedor; null = usa el calculado */
|
||||
numMensualidades: number | null
|
||||
setNumMensualidades: (n: number | null) => void
|
||||
/** Número efectivo de mensualidades (la elegida o la calculada) */
|
||||
/** 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
|
||||
propuestas: PropuestaResultado[]
|
||||
/** Aviso cuando el admin desactivó la propuesta que estaba elegida; null si no aplica */
|
||||
avisoPropuestaDesactivada: string | null
|
||||
/** Restaura el estado completo desde una cotización guardada (historial) */
|
||||
restaurar: (estado: EstadoRestaurar) => void
|
||||
/** Reinicia la cotización (botón "Nueva") */
|
||||
@@ -61,60 +48,34 @@ export interface EstadoRestaurar {
|
||||
unidad: Unidad
|
||||
acabados: boolean
|
||||
apartado: number
|
||||
/** Descuento (fracción) de la propuesta elegida; se aplica sobre su clave */
|
||||
descuento?: number
|
||||
propuestaIndex?: number
|
||||
/** Mensualidades elegidas al guardar la cotización (null = usar el plazo calculado) */
|
||||
numMensualidades?: number | null
|
||||
/** Mensualidades del diferimiento guardadas (default 14) */
|
||||
numMensualidades?: number
|
||||
/** Escrituración diferida guardada (default false) */
|
||||
escrituracionDiferida?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Estado central de la cotización: desarrollo, unidad, acabados, apartado y
|
||||
* descuentos. Las reglas comerciales llegan del API (con respaldo en las
|
||||
* constantes): si el admin desactiva una propuesta, deja de ofrecerse y, si era
|
||||
* la elegida, se avisa y se cae a la primera vigente.
|
||||
* 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_DEFAULT)
|
||||
const [enganchePct, setEnganchePct] = useState<number | null>(null)
|
||||
const [descuentos, setDescuentos] = useState<DescuentosPropuestas>(
|
||||
descuentosDefaultEfectivo(reglas, DESARROLLO_DEFAULT),
|
||||
)
|
||||
const [propuestaIndex, setPropuestaIndex] = useState(0)
|
||||
const [numMensualidades, setNumMensualidades] = useState<number | null>(null)
|
||||
const [avisoPropuestaDesactivada, setAvisoPropuestaDesactivada] = useState<string | null>(null)
|
||||
const [apartado, setApartado] = useState<number>(APARTADO_ESQUEMA_DEFAULT)
|
||||
const [numMensualidades, setNumMensualidades] = useState<number>(MENSUALIDADES_DEFAULT)
|
||||
const [escrituracionDiferida, setEscrituracionDiferida] = useState(false)
|
||||
|
||||
const params = desarrolloEfectivo(reglas, desarrollo)
|
||||
|
||||
// Los descuentos por defecto se re-aplican cuando cambian las reglas del
|
||||
// desarrollo (p. ej. al llegar las del API), sin pisar lo que capturó el
|
||||
// vendedor mientras las reglas no cambian.
|
||||
const descuentosAplicados = useRef<string>('')
|
||||
const defaults = useMemo(
|
||||
() => descuentosDefaultEfectivo(reglas, desarrollo),
|
||||
[reglas, desarrollo],
|
||||
)
|
||||
useEffect(() => {
|
||||
const clave = JSON.stringify(defaults)
|
||||
if (descuentosAplicados.current === clave) return
|
||||
descuentosAplicados.current = clave
|
||||
setDescuentos(defaults)
|
||||
}, [defaults])
|
||||
|
||||
const setDesarrollo = (id: string) => {
|
||||
setDesarrolloState(id)
|
||||
setUnidad(null)
|
||||
setAcabados(false)
|
||||
setDescuentos(descuentosDefaultEfectivo(reglas, id))
|
||||
setPropuestaIndex(0)
|
||||
setNumMensualidades(null)
|
||||
}
|
||||
|
||||
const setDescuento = (clave: keyof DescuentosPropuestas, valor: number) => {
|
||||
setDescuentos((prev) => ({ ...prev, [clave]: valor }))
|
||||
}
|
||||
|
||||
const restaurar = (estado: EstadoRestaurar) => {
|
||||
@@ -122,38 +83,23 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
||||
setUnidad(estado.unidad)
|
||||
setAcabados(estado.acabados)
|
||||
setApartado(estado.apartado)
|
||||
const base = descuentosDefaultEfectivo(reglas, estado.desarrollo)
|
||||
const paramsRestaurados = desarrolloEfectivo(reglas, estado.desarrollo)
|
||||
const vigentes = propuestasVigentes(paramsRestaurados)
|
||||
if (estado.descuento !== undefined) {
|
||||
const idx = estado.propuestaIndex ?? 0
|
||||
const clave = vigentes[idx]?.claveDescuento
|
||||
if (clave) base[clave] = estado.descuento
|
||||
}
|
||||
setDescuentos(base)
|
||||
setPropuestaIndex(estado.propuestaIndex ?? 0)
|
||||
setNumMensualidades(estado.numMensualidades ?? null)
|
||||
setNumMensualidades(estado.numMensualidades ?? MENSUALIDADES_DEFAULT)
|
||||
setEscrituracionDiferida(estado.escrituracionDiferida ?? false)
|
||||
}
|
||||
|
||||
const nueva = () => {
|
||||
setUnidad(null)
|
||||
setAcabados(false)
|
||||
setApartado(APARTADO_DEFAULT)
|
||||
setEnganchePct(null)
|
||||
setDescuentos(descuentosDefaultEfectivo(reglas, desarrollo))
|
||||
setPropuestaIndex(0)
|
||||
setNumMensualidades(null)
|
||||
setAvisoPropuestaDesactivada(null)
|
||||
setApartado(APARTADO_ESQUEMA_DEFAULT)
|
||||
setNumMensualidades(MENSUALIDADES_DEFAULT)
|
||||
setEscrituracionDiferida(false)
|
||||
}
|
||||
|
||||
const numMesesCalculado = useMemo(
|
||||
const numMeses = useMemo(
|
||||
() => (unidad ? resolverNumMensualidades(unidad, params) : null),
|
||||
[unidad, params],
|
||||
)
|
||||
|
||||
/** Número efectivo de mensualidades: la elegida por el vendedor, o la calculada */
|
||||
const numMeses = numMensualidades ?? numMesesCalculado
|
||||
|
||||
const precioLista = useMemo(
|
||||
() => (unidad ? obtenerPrecioLista(unidad, acabados) : 0),
|
||||
[unidad, acabados],
|
||||
@@ -163,46 +109,15 @@ 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)
|
||||
|
||||
// Enganche personalizado: null = default del Excel; inválido no entra al cálculo
|
||||
const errorEnganche = validarEnganchePct(enganchePct)
|
||||
const engancheEfectivo = errorEnganche === null && enganchePct !== null ? enganchePct / 100 : null
|
||||
|
||||
const propuestas = useMemo(() => {
|
||||
if (!unidad) return []
|
||||
return calcularPropuestas({
|
||||
unidad,
|
||||
acabados,
|
||||
const resultado = useMemo<EsquemaFrasaResultado | null>(() => {
|
||||
if (!unidad || precioLista <= 0) return null
|
||||
return calcularEsquemaFrasa({
|
||||
precio: precioLista,
|
||||
apartado: apartadoEfectivo,
|
||||
descuentos,
|
||||
desarrollo,
|
||||
reglas: params,
|
||||
enganchePct: engancheEfectivo,
|
||||
numMeses: numMeses ?? undefined,
|
||||
numMensualidades,
|
||||
escrituracionDiferida,
|
||||
})
|
||||
}, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, params, engancheEfectivo, numMeses])
|
||||
|
||||
// Propuesta elegida: si el índice se sale de la lista vigente (p. ej. el admin
|
||||
// desactivó una propuesta) se cae a la primera y, si la que estaba elegida ya
|
||||
// no existe, se avisa con claridad.
|
||||
const propuestaActiva = propuestas[Math.min(propuestaIndex, Math.max(propuestas.length - 1, 0))]
|
||||
const tipoPrevio = useRef<{ tipo: string; titulo: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const previo = tipoPrevio.current
|
||||
const vigentes = propuestasVigentes(params)
|
||||
if (previo !== null && !vigentes.some((p) => p.tipo === previo.tipo)) {
|
||||
const sustituto = propuestas[0]?.titulo ?? 'otra propuesta'
|
||||
setAvisoPropuestaDesactivada(
|
||||
`El administrador desactivó la propuesta ${previo.titulo} para ${params.nombre}. Se seleccionó ${sustituto}; elige otra si lo prefieres.`,
|
||||
)
|
||||
setPropuestaIndex(0)
|
||||
} else if (propuestaIndex >= propuestas.length) {
|
||||
setPropuestaIndex(0)
|
||||
}
|
||||
tipoPrevio.current = propuestaActiva
|
||||
? { tipo: propuestaActiva.tipo, titulo: propuestaActiva.titulo }
|
||||
: null
|
||||
}, [params, propuestas, propuestaActiva, propuestaIndex])
|
||||
}, [unidad, precioLista, apartadoEfectivo, numMensualidades, escrituracionDiferida])
|
||||
|
||||
return {
|
||||
desarrollo,
|
||||
@@ -215,19 +130,13 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
||||
apartado,
|
||||
setApartado,
|
||||
apartadoEfectivo,
|
||||
enganchePct,
|
||||
setEnganchePct,
|
||||
errorEnganche,
|
||||
descuentos,
|
||||
setDescuento,
|
||||
propuestaIndex,
|
||||
setPropuestaIndex,
|
||||
precioLista,
|
||||
numMensualidades,
|
||||
setNumMensualidades,
|
||||
escrituracionDiferida,
|
||||
setEscrituracionDiferida,
|
||||
precioLista,
|
||||
resultado,
|
||||
numMeses,
|
||||
propuestas,
|
||||
avisoPropuestaDesactivada,
|
||||
restaurar,
|
||||
nueva,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user