feat(cotizador): enganche adicional hasta 90% de anticipo total (UI, opciones de credito, PDF, API y persistencia)

This commit is contained in:
2026-09-29 21:08:02 +00:00
parent ed367582c3
commit 013e13b9df
15 changed files with 309 additions and 12 deletions

View File

@@ -516,3 +516,27 @@ describe('alertasInventarioEndpoint (POST /api/cotizaciones/alertas-inventario)
expect(prismaMock.actividadLocal.create).not.toHaveBeenCalled()
})
})
describe('crearCotizacion — enganche adicional', () => {
it('persiste el enganche adicional y recalcula el saldo a financiar', async () => {
const { res, json } = resFake()
await crearCotizacion(reqFake({ ...bodyBase, engancheAdicional: 400000 }), res)
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
data: expect.objectContaining({ engancheAdicional: 400000 }),
})
const respuesta = json.mock.calls[0]?.[0] as { esquema: { engancheAdicional: number; saldoAFinanciar: number } }
expect(respuesta.esquema.engancheAdicional).toBe(400000)
expect(respuesta.esquema.saldoAFinanciar).toBeCloseTo(445000, 2)
})
it('rechaza con 400 un adicional que pase del 90% de enganche total', async () => {
const { res } = resFake()
await expect(
crearCotizacion(reqFake({ ...bodyBase, engancheAdicional: 900000 }), res),
).rejects.toBeDefined()
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
})
})

View File

@@ -41,6 +41,9 @@ const crearCotizacionSchema = z.object({
.max(MENSUALIDADES_MAX, `Máximo ${MENSUALIDADES_MAX} mensualidades`)
.optional(),
escrituracionDiferida: z.boolean().optional(),
// Enganche adicional (anticipo extra): el total de anticipos (esquema +
// adicional) no puede superar el 90% del precio; el espejo lo valida
engancheAdicional: z.number().min(0, 'El enganche adicional no puede ser negativo').optional(),
// Opciones de credito del saldo (84.5%): JSON serializado {elegida, ajustes}
// que manda el cotizador; se persiste crudo y se devuelve en los GET
opcionesCredito: z.string().max(4000).optional(),
@@ -117,12 +120,21 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
const numMensualidades = Math.min(body.numMensualidades ?? MENSUALIDADES_DEFAULT, topeMensualidades)
const escrituracionDiferida = body.escrituracionDiferida ?? false
const precioLista = obtenerPrecioLista(unidad, body.acabados)
const esquema: EsquemaFrasaResultado = calcularEsquemaFrasa({
precio: precioLista,
apartado,
numMensualidades,
escrituracionDiferida,
})
// Enganche adicional: el total de anticipos no puede pasar del 90% del
// precio (espejo valida; se traduce a 400 con el mensaje del negocio)
const esquema: EsquemaFrasaResultado = (() => {
try {
return calcularEsquemaFrasa({
precio: precioLista,
apartado,
numMensualidades,
escrituracionDiferida,
...(body.engancheAdicional !== undefined ? { engancheAdicional: body.engancheAdicional } : {}),
})
} catch (err) {
throw new AppError(400, err instanceof Error ? err.message : 'Datos del esquema inválidos')
}
})()
const cotizacion = await prisma.cotizacion.create({
data: {
@@ -143,6 +155,7 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
pctEscrituracion: PCT_ESCRITURACION,
escrituracionDiferida,
escrituracion: esquema.escrituracion,
engancheAdicional: esquema.engancheAdicional,
// Opciones de credito elegidas por el vendedor (JSON crudo)
opcionesCredito: body.opcionesCredito ?? null,
// Columnas legacy, rellenadas para compatibilidad de reportes: