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:
2026-09-29 20:50:25 +00:00
parent 4e3ce67ae7
commit ed367582c3
7 changed files with 94 additions and 30 deletions

View File

@@ -43,7 +43,7 @@ const prismaMock = vi.hoisted(() => ({
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
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
@@ -260,7 +260,7 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
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([
{ unidadCodigo: 'U-101', campo: 'habitabilidad', valor: '2027-10-08' },
])
@@ -268,14 +268,15 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
await crearCotizacion(reqFake(bodyBase), res)
const esperadas = Math.min(14, calcularNumMensualidades('2027-10-08'))
const respuesta = json.mock.calls[0]?.[0] as {
numMensualidades: number
mensualidad: 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.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 () => {
@@ -299,6 +300,24 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
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 () => {
const opcionesCredito = JSON.stringify({
elegida: 'CONTADO',

View File

@@ -1,7 +1,7 @@
import type { Request, Response } from 'express'
import { z } from 'zod'
import { getUnidadLocal } from '../services/unidades-local.service.js'
import { obtenerPrecioLista } from '../services/cotizacion.service.js'
import { calcularNumMensualidades, obtenerPrecioLista } from '../services/cotizacion.service.js'
import {
calcularEsquemaFrasa,
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
// unidad: no se confía en los montos que manda el cliente.
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 precioLista = obtenerPrecioLista(unidad, body.acabados)
const esquema: EsquemaFrasaResultado = calcularEsquemaFrasa({