diff --git a/api-gateway/src/controllers/cotizaciones.controller.test.ts b/api-gateway/src/controllers/cotizaciones.controller.test.ts index bcf2efe..a0d1d23 100644 --- a/api-gateway/src/controllers/cotizaciones.controller.test.ts +++ b/api-gateway/src/controllers/cotizaciones.controller.test.ts @@ -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', diff --git a/api-gateway/src/controllers/cotizaciones.controller.ts b/api-gateway/src/controllers/cotizaciones.controller.ts index f946cd0..6927d15 100644 --- a/api-gateway/src/controllers/cotizaciones.controller.ts +++ b/api-gateway/src/controllers/cotizaciones.controller.ts @@ -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 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): * 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 - * del diferimiento: número de mensualidades (1..36) y si la escrituración se - * difiere con el enganche o se paga a la firma. + * del diferimiento: número de mensualidades (1..tope, donde el tope son los + * 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({ esquema, fechaEntrega, + maxMensualidades = MENSUALIDADES_MAX, numMensualidades, onNumMensualidades, escrituracionDiferida, onEscrituracionDiferida, }: 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 (
{/* Línea de tiempo del esquema */} @@ -135,11 +143,11 @@ export function EsquemaPanel({

{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)}` - : `De ${MENSUALIDADES_MIN} a ${MENSUALIDADES_MAX}; 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 ${tope}; cada una de ${formatoMoneda(esquema.mensualidad)}`}