feat(cotizador): 4 opciones de credito sobre el esquema FRASA (saldo 84.5% con mensualidad estimada)
- Nuevo modulo puro lib/opciones-credito.ts: Bancario, Infonavit-Cofinavit,
Semi Contado (1%) y Contado (2%), con descuentos del Excel del cliente.
Cada opcion recalcula el esquema FRASA 2026 sobre el precio final y
estima la mensualidad del credito (anualidad con tasa y plazo ajustables;
% de contado para la Semi). Ajustes validados (tasa 0-50%, plazo 1-35).
- Nuevo componente OpcionesCredito: grid de 4 tarjetas con tasa/plazo/% de
contado editables, mensualidad estimada y boton 'Usar en el PDF'.
- CotizadorPage: estado de ajustes y opcion elegida, memo con try/catch
(defaults de respaldo), restauracion defensiva al retomar cotizacion.
- PDF: seccion 'PROPUESTA DE FINANCIAMIENTO ELEGIDA' con renglones del
credito (o pago de contado y ahorro para Contado) + disclaimer.
- API: columna Cotizacion.opcionesCredito (TEXT, JSON {elegida, ajustes})
con migracion manual aplicada en SQLite; zod max 4000; se persiste crudo
y los GET devuelven la fila completa.
Tests: cotizador 149/149 (19 nuevos en opciones-credito, 2 en pdf),
API 746/746 (3 nuevos en cotizaciones). Smoke real POST+GET en produccion
local con unidad ATELIER-34 (201 con opcionesCredito; fila de prueba borrada).
This commit is contained in:
@@ -299,6 +299,47 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
|
||||
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('persiste y devuelve las opciones de crédito que manda el cotizador', async () => {
|
||||
const opcionesCredito = JSON.stringify({
|
||||
elegida: 'CONTADO',
|
||||
ajustes: {
|
||||
CREDITO_BANCARIO: { tasaAnual: 0.105, plazoAnos: 20 },
|
||||
CREDITO_INFONAVIT_COFINAVIT: { tasaAnual: 0.105, plazoAnos: 25 },
|
||||
SEMI_CONTADO: { tasaAnual: 0.105, plazoAnos: 20, pctContado: 0.5 },
|
||||
CONTADO: { tasaAnual: 0, plazoAnos: 1 },
|
||||
},
|
||||
})
|
||||
const { res, status, json } = resFake()
|
||||
|
||||
await crearCotizacion(reqFake({ ...bodyBase, opcionesCredito }), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ opcionesCredito }),
|
||||
})
|
||||
// La respuesta del POST incluye el campo (los GET devuelven la fila completa)
|
||||
expect(json.mock.calls[0]?.[0]).toMatchObject({ opcionesCredito })
|
||||
})
|
||||
|
||||
it('sin opciones de crédito persiste null', async () => {
|
||||
const { res } = resFake()
|
||||
|
||||
await crearCotizacion(reqFake(bodyBase), res)
|
||||
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ opcionesCredito: null }),
|
||||
})
|
||||
})
|
||||
|
||||
it('rechaza opciones de crédito que exceden 4000 caracteres', async () => {
|
||||
const { res } = resFake()
|
||||
|
||||
await expect(
|
||||
crearCotizacion(reqFake({ ...bodyBase, opcionesCredito: 'x'.repeat(4001) }), res),
|
||||
).rejects.toBeDefined()
|
||||
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('unidad no DISPONIBLE (apartada/vendida) → 409 y no se crea cotización', async () => {
|
||||
prismaMock.unidad.findUnique.mockResolvedValue({ ...filaUnidad, codigo: 'ATELIER-10', status: 'VENDIDO' })
|
||||
const { res } = resFake()
|
||||
|
||||
Reference in New Issue
Block a user