feat(cotizador): tope de mensualidades = meses que faltan para la entrega (UI y API); nunca se rebasa la fecha de entrega
This commit is contained in:
@@ -43,7 +43,7 @@ const prismaMock = vi.hoisted(() => ({
|
|||||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||||
|
|
||||||
import { alertasInventarioEndpoint, crearCotizacion } from './cotizaciones.controller.js'
|
import { alertasInventarioEndpoint, crearCotizacion } from './cotizaciones.controller.js'
|
||||||
import { DESARROLLOS } from '../services/cotizacion.service.js'
|
import { DESARROLLOS, calcularNumMensualidades } from '../services/cotizacion.service.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cotizaciones 100% locales: la unidad sale del espejo (tabla Unidad), la
|
* Cotizaciones 100% locales: la unidad sale del espejo (tabla Unidad), la
|
||||||
@@ -260,7 +260,7 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
|
|||||||
expect(prismaMock.leadCaptura.findFirst).not.toHaveBeenCalled()
|
expect(prismaMock.leadCaptura.findFirst).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sin numMensualidades usa el default del esquema (14)', async () => {
|
it('sin numMensualidades usa el default (14), acotado a los meses que faltan para la entrega', async () => {
|
||||||
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||||
{ unidadCodigo: 'U-101', campo: 'habitabilidad', valor: '2027-10-08' },
|
{ unidadCodigo: 'U-101', campo: 'habitabilidad', valor: '2027-10-08' },
|
||||||
])
|
])
|
||||||
@@ -268,14 +268,15 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
|
|||||||
|
|
||||||
await crearCotizacion(reqFake(bodyBase), res)
|
await crearCotizacion(reqFake(bodyBase), res)
|
||||||
|
|
||||||
|
const esperadas = Math.min(14, calcularNumMensualidades('2027-10-08'))
|
||||||
const respuesta = json.mock.calls[0]?.[0] as {
|
const respuesta = json.mock.calls[0]?.[0] as {
|
||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
mensualidad: number
|
mensualidad: number
|
||||||
esquema: { numMensualidades: number; mensualidad: number; baseDiferible: number }
|
esquema: { numMensualidades: number; mensualidad: number; baseDiferible: number }
|
||||||
}
|
}
|
||||||
expect(respuesta.numMensualidades).toBe(14)
|
expect(respuesta.numMensualidades).toBe(esperadas)
|
||||||
expect(respuesta.numMensualidades).toBe(respuesta.esquema.numMensualidades)
|
expect(respuesta.numMensualidades).toBe(respuesta.esquema.numMensualidades)
|
||||||
expect(respuesta.esquema.mensualidad).toBeCloseTo(respuesta.esquema.baseDiferible / 14, 6)
|
expect(respuesta.esquema.mensualidad).toBeCloseTo(respuesta.esquema.baseDiferible / esperadas, 6)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('las mensualidades elegidas por el vendedor viajan al cálculo y se persisten', async () => {
|
it('las mensualidades elegidas por el vendedor viajan al cálculo y se persisten', async () => {
|
||||||
@@ -299,6 +300,24 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
|
|||||||
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('acota las mensualidades a los meses que faltan para la entrega estimada de la unidad', async () => {
|
||||||
|
// La captura de obra marca la entrega de U-101 a ~6 meses (fija vs hoy)
|
||||||
|
const entrega = new Date(Date.now() + 6 * 30.4 * 86400000).toISOString().slice(0, 10)
|
||||||
|
prismaMock.capturaDepartamento.findMany.mockResolvedValue([
|
||||||
|
{ unidadCodigo: 'U-101', campo: 'entrega_programada_fecha', valor: entrega },
|
||||||
|
])
|
||||||
|
const { res, json } = resFake()
|
||||||
|
|
||||||
|
await crearCotizacion(reqFake({ ...bodyBase, numMensualidades: 20 }), res)
|
||||||
|
|
||||||
|
const respuesta = json.mock.calls[0]?.[0] as { numMensualidades: number; esquema: { numMensualidades: number } }
|
||||||
|
expect(respuesta.numMensualidades).toBe(6)
|
||||||
|
expect(respuesta.esquema.numMensualidades).toBe(6)
|
||||||
|
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||||
|
data: expect.objectContaining({ numMensualidades: 6 }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('persiste y devuelve las opciones de crédito que manda el cotizador', async () => {
|
it('persiste y devuelve las opciones de crédito que manda el cotizador', async () => {
|
||||||
const opcionesCredito = JSON.stringify({
|
const opcionesCredito = JSON.stringify({
|
||||||
elegida: 'CONTADO',
|
elegida: 'CONTADO',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Request, Response } from 'express'
|
import type { Request, Response } from 'express'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { getUnidadLocal } from '../services/unidades-local.service.js'
|
import { getUnidadLocal } from '../services/unidades-local.service.js'
|
||||||
import { obtenerPrecioLista } from '../services/cotizacion.service.js'
|
import { calcularNumMensualidades, obtenerPrecioLista } from '../services/cotizacion.service.js'
|
||||||
import {
|
import {
|
||||||
calcularEsquemaFrasa,
|
calcularEsquemaFrasa,
|
||||||
MENSUALIDADES_DEFAULT,
|
MENSUALIDADES_DEFAULT,
|
||||||
@@ -109,7 +109,12 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
|
|||||||
// El API RECALCULA el esquema server-side con el precio de lista de la
|
// El API RECALCULA el esquema server-side con el precio de lista de la
|
||||||
// unidad: no se confía en los montos que manda el cliente.
|
// unidad: no se confía en los montos que manda el cliente.
|
||||||
const apartado = body.apartado ?? APARTADO_DEFAULT
|
const apartado = body.apartado ?? APARTADO_DEFAULT
|
||||||
const numMensualidades = body.numMensualidades ?? MENSUALIDADES_DEFAULT
|
// El diferimiento no puede rebasar la entrega estimada de la unidad (captura
|
||||||
|
// de obra): se acota a los meses que faltan (36 sigue siendo el tope global)
|
||||||
|
const topeMensualidades = unidad.fechaEntrega
|
||||||
|
? Math.min(MENSUALIDADES_MAX, Math.max(MENSUALIDADES_MIN, calcularNumMensualidades(unidad.fechaEntrega)))
|
||||||
|
: MENSUALIDADES_MAX
|
||||||
|
const numMensualidades = Math.min(body.numMensualidades ?? MENSUALIDADES_DEFAULT, topeMensualidades)
|
||||||
const escrituracionDiferida = body.escrituracionDiferida ?? false
|
const escrituracionDiferida = body.escrituracionDiferida ?? false
|
||||||
const precioLista = obtenerPrecioLista(unidad, body.acabados)
|
const precioLista = obtenerPrecioLista(unidad, body.acabados)
|
||||||
const esquema: EsquemaFrasaResultado = calcularEsquemaFrasa({
|
const esquema: EsquemaFrasaResultado = calcularEsquemaFrasa({
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ interface Props {
|
|||||||
esquema: EsquemaFrasaResultado
|
esquema: EsquemaFrasaResultado
|
||||||
/** Fecha de entrega estimada de la unidad (captura de obra); alimenta la nota del diferimiento */
|
/** Fecha de entrega estimada de la unidad (captura de obra); alimenta la nota del diferimiento */
|
||||||
fechaEntrega?: string | null
|
fechaEntrega?: string | null
|
||||||
|
/** Tope de mensualidades: los meses que faltan para la entrega (default 36 sin fecha) */
|
||||||
|
maxMensualidades?: number
|
||||||
numMensualidades: number
|
numMensualidades: number
|
||||||
onNumMensualidades: (n: number) => void
|
onNumMensualidades: (n: number) => void
|
||||||
escrituracionDiferida: boolean
|
escrituracionDiferida: boolean
|
||||||
@@ -28,17 +30,23 @@ const pasoMonto = 'mt-1 text-lg font-semibold text-[#111827] dark:text-[#E2E8F0]
|
|||||||
* Panel único del esquema FRASA 2026 (reemplaza a las 4 propuestas):
|
* Panel único del esquema FRASA 2026 (reemplaza a las 4 propuestas):
|
||||||
* línea de tiempo Hoy → A la firma → Mensualidades, el detalle de conceptos
|
* línea de tiempo Hoy → A la firma → Mensualidades, el detalle de conceptos
|
||||||
* (enganche de firma 2.5%, enganche 7.5%, escrituración 5.5%) y los controles
|
* (enganche de firma 2.5%, enganche 7.5%, escrituración 5.5%) y los controles
|
||||||
* del diferimiento: número de mensualidades (1..36) y si la escrituración se
|
* del diferimiento: número de mensualidades (1..tope, donde el tope son los
|
||||||
* difiere con el enganche o se paga a la firma.
|
* meses que faltan para la entrega estimada de la unidad) y si la escrituración
|
||||||
|
* se difiere con el enganche o se paga a la firma.
|
||||||
*/
|
*/
|
||||||
export function EsquemaPanel({
|
export function EsquemaPanel({
|
||||||
esquema,
|
esquema,
|
||||||
fechaEntrega,
|
fechaEntrega,
|
||||||
|
maxMensualidades = MENSUALIDADES_MAX,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
onNumMensualidades,
|
onNumMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
onEscrituracionDiferida,
|
onEscrituracionDiferida,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
// El diferimiento no puede rebasar la entrega de la vivienda: se ofrecen
|
||||||
|
// sólo las mensualidades dentro del tope (con fecha, meses que faltan)
|
||||||
|
const tope = Math.max(MENSUALIDADES_MIN, Math.min(MENSUALIDADES_MAX, maxMensualidades))
|
||||||
|
const mensualidadEfectiva = Math.min(numMensualidades, tope)
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Línea de tiempo del esquema */}
|
{/* Línea de tiempo del esquema */}
|
||||||
@@ -135,11 +143,11 @@ export function EsquemaPanel({
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="mensualidades-esquema"
|
id="mensualidades-esquema"
|
||||||
value={numMensualidades}
|
value={mensualidadEfectiva}
|
||||||
onChange={(e) => onNumMensualidades(Number(e.target.value))}
|
onChange={(e) => onNumMensualidades(Math.min(Number(e.target.value), tope))}
|
||||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none dark:bg-[#0F172A] dark:border-[#334155] dark:text-[#E2E8F0]"
|
||||||
>
|
>
|
||||||
{Array.from({ length: MENSUALIDADES_MAX - MENSUALIDADES_MIN + 1 }, (_, i) => MENSUALIDADES_MIN + i).map(
|
{Array.from({ length: tope - MENSUALIDADES_MIN + 1 }, (_, i) => MENSUALIDADES_MIN + i).map(
|
||||||
(n) => (
|
(n) => (
|
||||||
<option key={n} value={n}>
|
<option key={n} value={n}>
|
||||||
{n} mensualidades
|
{n} mensualidades
|
||||||
@@ -149,8 +157,8 @@ export function EsquemaPanel({
|
|||||||
</select>
|
</select>
|
||||||
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||||
{fechaEntrega
|
{fechaEntrega
|
||||||
? `Sugeridas por la entrega estimada (${new Date(`${fechaEntrega}T00:00:00`).toLocaleDateString('es-MX', { day: 'numeric', month: 'short', year: 'numeric' })}); cada una de ${formatoMoneda(esquema.mensualidad)}`
|
? `Tope: la entrega estimada es el ${new Date(`${fechaEntrega}T00:00:00`).toLocaleDateString('es-MX', { day: 'numeric', month: 'short', year: 'numeric' })} (máx. ${tope} mensualidades); cada una de ${formatoMoneda(esquema.mensualidad)}`
|
||||||
: `De ${MENSUALIDADES_MIN} a ${MENSUALIDADES_MAX}; cada una de ${formatoMoneda(esquema.mensualidad)}`}
|
: `De ${MENSUALIDADES_MIN} a ${tope}; cada una de ${formatoMoneda(esquema.mensualidad)}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<label className="flex cursor-pointer items-center gap-2 pb-2.5 text-sm text-[#374151] dark:text-[#CBD5E1]">
|
<label className="flex cursor-pointer items-center gap-2 pb-2.5 text-sm text-[#374151] dark:text-[#CBD5E1]">
|
||||||
|
|||||||
@@ -105,13 +105,24 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
|||||||
|
|
||||||
it('las mensualidades elegidas recalculan la mensualidad sobre el diferible', () => {
|
it('las mensualidades elegidas recalculan la mensualidad sobre el diferible', () => {
|
||||||
const { result } = montar()
|
const { result } = montar()
|
||||||
|
const n = Math.min(6, mensualidadesParaUnidad(unidadDiagonal))
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.setNumMensualidades(12)
|
result.current.setNumMensualidades(n)
|
||||||
})
|
})
|
||||||
const r = result.current.resultado!
|
const r = result.current.resultado!
|
||||||
expect(r.numMensualidades).toBe(12)
|
expect(r.numMensualidades).toBe(n)
|
||||||
expect(r.mensualidad).toBeCloseTo(r.baseDiferible / 12, 6)
|
expect(r.mensualidad).toBeCloseTo(r.baseDiferible / n, 6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('el ajuste de mensualidades respeta el tope de la entrega estimada', () => {
|
||||||
|
const { result } = montar()
|
||||||
|
const tope = mensualidadesParaUnidad(unidadDiagonal)
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setNumMensualidades(tope + 10)
|
||||||
|
})
|
||||||
|
expect(result.current.numMensualidades).toBe(tope)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('escrituración diferida: sube el diferible y baja el pago a la firma', () => {
|
it('escrituración diferida: sube el diferible y baja el pago a la firma', () => {
|
||||||
@@ -126,8 +137,9 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
|||||||
expect(r.pagoUnicoFirma).toBeCloseTo(r.saldoFirma, 6)
|
expect(r.pagoUnicoFirma).toBeCloseTo(r.saldoFirma, 6)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('restaurar carga una cotización guardada del esquema (N y escrituración)', () => {
|
it('restaurar carga una cotización guardada (N acotado a la entrega y escrituración)', () => {
|
||||||
const { result } = montar()
|
const { result } = montar()
|
||||||
|
const tope = mensualidadesParaUnidad(unidadDiagonal)
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.restaurar({
|
result.current.restaurar({
|
||||||
@@ -141,12 +153,13 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
|||||||
})
|
})
|
||||||
expect(result.current.acabados).toBe(true)
|
expect(result.current.acabados).toBe(true)
|
||||||
expect(result.current.apartadoEfectivo).toBe(20000)
|
expect(result.current.apartadoEfectivo).toBe(20000)
|
||||||
expect(result.current.numMensualidades).toBe(24)
|
// El diferimiento no puede rebasar la entrega: 24 se acota a las que faltan
|
||||||
|
expect(result.current.numMensualidades).toBe(tope)
|
||||||
expect(result.current.escrituracionDiferida).toBe(true)
|
expect(result.current.escrituracionDiferida).toBe(true)
|
||||||
expect(result.current.resultado?.numMensualidades).toBe(24)
|
expect(result.current.resultado?.numMensualidades).toBe(tope)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('restaurar una cotización legacy (sin N ni escrituración) cae a los defaults', () => {
|
it('restaurar una cotización legacy (sin N ni escrituración): default acotado a la entrega', () => {
|
||||||
const { result } = montar()
|
const { result } = montar()
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -157,7 +170,10 @@ describe('useCotizacion — esquema único FRASA 2026', () => {
|
|||||||
apartado: 20000,
|
apartado: 20000,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
expect(result.current.numMensualidades).toBe(MENSUALIDADES_DEFAULT)
|
// Sin N guardado cae al default (14), pero nunca rebasando la entrega
|
||||||
|
expect(result.current.numMensualidades).toBe(
|
||||||
|
Math.min(MENSUALIDADES_DEFAULT, mensualidadesParaUnidad(unidadDiagonal)),
|
||||||
|
)
|
||||||
expect(result.current.escrituracionDiferida).toBe(false)
|
expect(result.current.escrituracionDiferida).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ export const APARTADO_ESQUEMA_DEFAULT = 15000
|
|||||||
const MS_POR_MES = 30.4 * 86400000
|
const MS_POR_MES = 30.4 * 86400000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mensualidades del diferimiento sugeridas para una unidad: los meses que
|
* Tope (y default) de mensualidades del diferimiento para una unidad: los
|
||||||
* faltan hasta su fecha de entrega estimada (la que vive en el módulo de
|
* meses que faltan hasta su fecha de entrega estimada (la que vive en el
|
||||||
* captura: entrega programada/reprogramada de obra). Si la unidad no trae
|
* módulo de captura: entrega programada/reprogramada de obra). El diferimiento
|
||||||
* fecha, cae al default del esquema (14). El vendedor puede ajustarla después.
|
* 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 {
|
export function mensualidadesParaUnidad(u: Unidad | null, hoy: Date = new Date()): number {
|
||||||
const fecha = u?.fechaEntrega
|
const fecha = u?.fechaEntrega
|
||||||
@@ -101,18 +102,29 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
|
|
||||||
/** Elige unidad y, con ella, las mensualidades del diferimiento: los meses
|
/** 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
|
* que faltan para la entrega estimada de esa vivienda (dato del módulo de
|
||||||
* captura). El vendedor puede ajustarlas después. */
|
* captura). El vendedor puede ajustarlas después, sin rebasar la entrega. */
|
||||||
const elegirUnidad = (u: Unidad | null) => {
|
const elegirUnidad = (u: Unidad | null) => {
|
||||||
setUnidad(u)
|
setUnidad(u)
|
||||||
setNumMensualidades(mensualidadesParaUnidad(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())))
|
||||||
|
}
|
||||||
|
|
||||||
const restaurar = (estado: EstadoRestaurar) => {
|
const restaurar = (estado: EstadoRestaurar) => {
|
||||||
setDesarrolloState(estado.desarrollo)
|
setDesarrolloState(estado.desarrollo)
|
||||||
setUnidad(estado.unidad)
|
setUnidad(estado.unidad)
|
||||||
setAcabados(estado.acabados)
|
setAcabados(estado.acabados)
|
||||||
setApartado(estado.apartado)
|
setApartado(estado.apartado)
|
||||||
setNumMensualidades(estado.numMensualidades ?? MENSUALIDADES_DEFAULT)
|
// 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)
|
setEscrituracionDiferida(estado.escrituracionDiferida ?? false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +172,7 @@ export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizac
|
|||||||
setApartado,
|
setApartado,
|
||||||
apartadoEfectivo,
|
apartadoEfectivo,
|
||||||
numMensualidades,
|
numMensualidades,
|
||||||
setNumMensualidades,
|
setNumMensualidades: ajustarMensualidades,
|
||||||
escrituracionDiferida,
|
escrituracionDiferida,
|
||||||
setEscrituracionDiferida,
|
setEscrituracionDiferida,
|
||||||
precioLista,
|
precioLista,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
|
|||||||
import { MemoryRouter } from 'react-router-dom'
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
import CotizadorPage from './CotizadorPage'
|
import CotizadorPage from './CotizadorPage'
|
||||||
import { INVENTARIO_SEED } from '../data/unidades'
|
import { INVENTARIO_SEED } from '../data/unidades'
|
||||||
|
import { mensualidadesParaUnidad } from '../hooks/useCotizacion'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smoke del cierre de la cotización con el esquema único FRASA 2026:
|
* Smoke del cierre de la cotización con el esquema único FRASA 2026:
|
||||||
@@ -70,7 +71,8 @@ describe('CotizadorPage — barra sticky y guardado local', () => {
|
|||||||
expect(await screen.findByText('Esquema FRASA 2026 — conceptos')).toBeTruthy()
|
expect(await screen.findByText('Esquema FRASA 2026 — conceptos')).toBeTruthy()
|
||||||
expect(screen.getByText('Hoy')).toBeTruthy()
|
expect(screen.getByText('Hoy')).toBeTruthy()
|
||||||
expect(screen.getByText('A la firma')).toBeTruthy()
|
expect(screen.getByText('A la firma')).toBeTruthy()
|
||||||
expect(screen.getByRole('option', { name: '14 mensualidades' })).toBeTruthy()
|
// El tope de mensualidades son los meses que faltan para la entrega
|
||||||
|
expect(screen.getByRole('option', { name: `${mensualidadesParaUnidad(unidad)} mensualidades` })).toBeTruthy()
|
||||||
|
|
||||||
// Guardar con el servidor caído → folio local persistido en el dispositivo
|
// Guardar con el servidor caído → folio local persistido en el dispositivo
|
||||||
fireEvent.change(screen.getByLabelText(/Nombre del cliente/), {
|
fireEvent.change(screen.getByLabelText(/Nombre del cliente/), {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { useUnidades } from '../hooks/useUnidades'
|
import { useUnidades } from '../hooks/useUnidades'
|
||||||
import { useCotizacion } from '../hooks/useCotizacion'
|
import { useCotizacion, mensualidadesParaUnidad } from '../hooks/useCotizacion'
|
||||||
|
import { MENSUALIDADES_MAX } from '../lib/esquema-frasa'
|
||||||
import DarkModeToggle from '../components/DarkModeToggle'
|
import DarkModeToggle from '../components/DarkModeToggle'
|
||||||
import { SkeletonList } from "../components/Skeleton"
|
import { SkeletonList } from "../components/Skeleton"
|
||||||
import toast from "react-hot-toast"
|
import toast from "react-hot-toast"
|
||||||
@@ -783,6 +784,7 @@ export default function CotizadorPage() {
|
|||||||
<EsquemaPanel
|
<EsquemaPanel
|
||||||
esquema={resultado}
|
esquema={resultado}
|
||||||
fechaEntrega={unidad?.fechaEntrega ?? null}
|
fechaEntrega={unidad?.fechaEntrega ?? null}
|
||||||
|
maxMensualidades={unidad ? mensualidadesParaUnidad(unidad) : MENSUALIDADES_MAX}
|
||||||
numMensualidades={numMensualidades}
|
numMensualidades={numMensualidades}
|
||||||
onNumMensualidades={setNumMensualidades}
|
onNumMensualidades={setNumMensualidades}
|
||||||
escrituracionDiferida={escrituracionDiferida}
|
escrituracionDiferida={escrituracionDiferida}
|
||||||
|
|||||||
Reference in New Issue
Block a user