import { beforeEach, describe, expect, it, vi } from 'vitest' // ---------- Prisma mockeado (los tests no tocan BD) ---------- // `reglaCotizadorDesarrollo` y `desarrollo` se activan/desactivan por test: // simulan el cliente Prisma con los modelos generados o sin ellos (lectura // cruda de las tablas). const prismaMock = vi.hoisted(() => ({ $queryRawUnsafe: vi.fn(), reglaCotizadorDesarrollo: undefined as undefined | { findMany: ReturnType }, desarrollo: undefined as undefined | { findMany: ReturnType }, })) vi.mock('./prisma.js', () => ({ prisma: prismaMock })) import { DESARROLLOS, type ParametrosDesarrollo } from './cotizacion.service.js' import { getReglasDesarrollo, getReglasEfectivas, getReglasGuardadas, normalizarReglas, } from './reglas-desarrollo.service.js' /** Reglas con la forma exacta que guarda la BD (JSON serializado) */ function reglasJson(params: ParametrosDesarrollo, extras: Record = {}): string { return JSON.stringify({ ...params, ...extras, propuestas: params.propuestas.map((p) => ({ ...p, activa: true })), }) } function usarModeloPrisma(): ReturnType { const findMany = vi.fn() prismaMock.reglaCotizadorDesarrollo = { findMany } return findMany } /** Ficha del desarrollo (tabla Desarrollo) para el catálogo del servicio */ function ficha(id: string, clave: string, extras: Record = {}) { return { id, clave, nombre: clave.charAt(0) + clave.slice(1).toLowerCase(), activoEnCotizador: true, prefijoUnidadPdf: 'CASA', ...extras, } } function usarCatalogo(filas: unknown[]): void { prismaMock.desarrollo = { findMany: vi.fn().mockResolvedValue(filas) } } /** Propuesta del fixture (JSON crudo) por tipo */ function brinda(propuestas: Record[], tipo: string): Record { const p = propuestas.find((x) => x['tipo'] === tipo) if (!p) throw new Error(`La propuesta ${tipo} no está en el fixture`) return p } beforeEach(() => { vi.clearAllMocks() prismaMock.reglaCotizadorDesarrollo = undefined prismaMock.desarrollo = undefined prismaMock.$queryRawUnsafe.mockReset() prismaMock.$queryRawUnsafe.mockResolvedValue([]) }) describe('normalizarReglas (JSON de la BD)', () => { it('acepta el JSON con la forma de las constantes y conserva los valores', () => { const reglas = normalizarReglas('DIAGONAL', JSON.parse(reglasJson(DESARROLLOS['DIAGONAL']!))) expect(reglas).not.toBeNull() expect(reglas?.id).toBe('DIAGONAL') expect(reglas?.propuestas).toHaveLength(4) expect(reglas?.propuestas[2]?.descuentoDefault).toBe(0.02) }) it('marca la propuesta desactivada por el admin (activa/activo)', () => { const bruto = JSON.parse(reglasJson(DESARROLLOS['SENDA']!)) as { propuestas: Record[] } brinda(bruto.propuestas, 'CONTADO').activa = false const reglas = normalizarReglas('SENDA', bruto) expect(reglas?.propuestas.find((p) => p.tipo === 'CONTADO')?.activa).toBe(false) }) it('rechaza JSON sin la forma esperada (precioUnico, propuestas, claves)', () => { expect(normalizarReglas('DIAGONAL', null)).toBeNull() expect(normalizarReglas('DIAGONAL', 'DIAGONAL')).toBeNull() expect(normalizarReglas('DIAGONAL', { precioUnico: true })).toBeNull() expect( normalizarReglas('DIAGONAL', { precioUnico: true, fuenteDeN: 'fijo', propuestas: [{ tipo: 'CONTADO' }], }), ).toBeNull() }) }) describe('getReglasGuardadas / getReglasEfectivas', () => { it('sin filas guardadas devuelve las constantes actuales (compatibilidad)', async () => { usarModeloPrisma().mockResolvedValue([]) const efectivas = await getReglasEfectivas() expect(Object.keys(efectivas).sort()).toEqual(Object.keys(DESARROLLOS).sort()) for (const [id, constantes] of Object.entries(DESARROLLOS)) { expect(efectivas[id]).toEqual(constantes) } expect(await getReglasDesarrollo('SENDA')).toEqual(DESARROLLOS['SENDA']) }) it('las reglas de la BD ganan a las constantes del desarrollo', async () => { const conDescuento = { ...DESARROLLOS['DIAGONAL']!, propuestas: DESARROLLOS['DIAGONAL']!.propuestas.map((p) => p.tipo === 'CONTADO' ? { ...p, descuentoDefault: 0.07 } : p, ), } usarModeloPrisma().mockResolvedValue([ { desarrolloId: 'DIAGONAL', parametrosJson: reglasJson(conDescuento) }, ]) const reglas = await getReglasDesarrollo('DIAGONAL') const contado = reglas.propuestas.find((p) => p.tipo === 'CONTADO') expect(contado?.descuentoDefault).toBe(0.07) expect(DESARROLLOS['DIAGONAL']!.propuestas[2]?.descuentoDefault).toBe(0.02) // Los demás desarrollos siguen en constantes expect((await getReglasDesarrollo('SOLANA')).propuestas[1]?.pctEnganche).toBe(0) }) it('usa la columna activoEnCotizador y también el campo dentro del JSON', async () => { usarModeloPrisma().mockResolvedValue([ { desarrolloId: 'SOLANA', parametrosJson: reglasJson(DESARROLLOS['SOLANA']!), activoEnCotizador: false, }, { desarrolloId: 'ESTANZA', parametrosJson: reglasJson(DESARROLLOS['ESTANZA']!, { activoEnCotizador: false }), }, ]) expect((await getReglasDesarrollo('SOLANA')).activoEnCotizador).toBe(false) expect((await getReglasDesarrollo('ESTANZA')).activoEnCotizador).toBe(false) expect((await getReglasDesarrollo('DIAGONAL')).activoEnCotizador).toBeUndefined() }) it('resuelve la clave del desarrollo por su id (las reglas apuntan al uuid) y lee su ficha', async () => { usarModeloPrisma().mockResolvedValue([ { desarrolloId: 'uuid-diagonal', parametrosJson: reglasJson({ ...DESARROLLOS['DIAGONAL']!, nombre: undefined as never }), }, ]) usarCatalogo([ ficha('uuid-diagonal', 'DIAGONAL', { activoEnCotizador: false, nombre: 'Diagonal Residencial' }), ficha('uuid-senda', 'SENDA'), ]) const reglas = await getReglasDesarrollo('DIAGONAL') // La clave sale del catálogo (id → clave), no del uuid expect(reglas.propuestas).toHaveLength(4) // El nombre y la bandera salen de la ficha del desarrollo expect(reglas.nombre).toBe('Diagonal Residencial') expect(reglas.activoEnCotizador).toBe(false) // Las reglas efectivas y el desarrollo activo siguen saliendo de constantes const efectivas = await getReglasEfectivas() expect(efectivas['SENDA']).toEqual(DESARROLLOS['SENDA']) expect(efectivas['SOLANA']?.propuestas[1]?.pctEnganche).toBe(0) }) it('ignora filas sin desarrollo identificable o sin parámetros', async () => { const sinClave = JSON.parse(reglasJson(DESARROLLOS['SENDA']!)) as Record delete sinClave['id'] usarModeloPrisma().mockResolvedValue([ { desarrolloId: '', parametrosJson: JSON.stringify(sinClave) }, { desarrolloId: 'SENDA', parametrosJson: null }, null, ]) expect((await getReglasGuardadas()).size).toBe(0) }) it('lee la tabla por SQL crudo cuando el cliente Prisma no tiene el modelo', async () => { prismaMock.$queryRawUnsafe.mockImplementation(async (sql: string) => { if (sql.includes('"ReglaCotizadorDesarrollo"')) { return [ { desarrolloId: 'uuid-senda', parametrosJson: reglasJson(DESARROLLOS['SENDA']!) }, { desarrollo: 'SOLANA', parametros: reglasJson(DESARROLLOS['SOLANA']!) }, ] } return [ficha('uuid-senda', 'SENDA'), ficha('uuid-solana', 'SOLANA')] }) const reglas = await getReglasEfectivas() expect(prismaMock.$queryRawUnsafe).toHaveBeenCalledWith( expect.stringContaining('"ReglaCotizadorDesarrollo"'), ) expect(reglas['SENDA']?.propuestas).toHaveLength(4) expect(reglas['SOLANA']?.propuestas[1]?.pctEnganche).toBe(0) }) }) describe('respaldo en constantes (nunca falla la cotización)', () => { it('JSON corrupto: respaldo en las constantes con warning', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) usarModeloPrisma().mockResolvedValue([ { desarrolloId: 'DIAGONAL', parametrosJson: '{"propuestas": [' }, ]) const reglas = await getReglasDesarrollo('DIAGONAL') expect(reglas).toEqual(DESARROLLOS['DIAGONAL']) expect(warn).toHaveBeenCalledWith(expect.stringContaining('JSON inválido')) warn.mockRestore() }) it('JSON válido pero con forma inesperada: respaldo en las constantes con warning', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) usarModeloPrisma().mockResolvedValue([ { desarrolloId: 'DIAGONAL', parametrosJson: JSON.stringify({ precioUnico: true }) }, ]) const reglas = await getReglasDesarrollo('DIAGONAL') expect(reglas).toEqual(DESARROLLOS['DIAGONAL']) expect(warn).toHaveBeenCalledWith(expect.stringContaining('forma inesperada')) warn.mockRestore() }) it('tabla inexistente: respaldo en las constantes, sin lanzar y con un solo warning', async () => { vi.resetModules() const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) prismaMock.$queryRawUnsafe.mockRejectedValue(new Error('no such table: ReglaCotizadorDesarrollo')) const servicio = await import('./reglas-desarrollo.service.js') const reglas = await servicio.getReglasDesarrollo('SENDA') const efectivas = await servicio.getReglasEfectivas() expect(reglas).toEqual(DESARROLLOS['SENDA']) expect(Object.keys(efectivas)).toHaveLength(Object.keys(DESARROLLOS).length) expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith(expect.stringContaining('constantes'), expect.any(Error)) warn.mockRestore() }) })