Files
FRASA-CAS/api-gateway/src/services/reglas-desarrollo.service.test.ts
Consult AS Technology d6548999bf feat(desarrollos): modulo de alta de desarrollos con inventario, reglas del cotizador por desarrollo y micrositio publico
Modulo nuevo (solo admin) para dar de alta desarrollos sin tocar codigo:
- CRUD de la ficha, modelos e inventario (alta manual y generador por rango)
- Reglas del cotizador por desarrollo, editables y persistidas en la BD
- El cotizador consume las reglas desde /desarrollos/reglas-cotizador y cae a
  las constantes con aviso cuando un desarrollo aun no tiene reglas guardadas
- Micrositio publico en /d/:clave (sin sesion) con ficha, modelos y
  disponibilidad; jamas expone datos de clientes ni cotizaciones

Integracion:
- La landing monta el modulo en el grid (clave 'desarrollos', solo admin) y el
  micrositio fuera del login; el rol manager NO accede (matriz y API con 403)
- La tabla de roles de Configuracion ahora lista todos los modulos
- Se alinea la FK de ReglaCotizadorDesarrollo con la BD viva (Desarrollo.id)

Verificado: 708 pruebas del API, 115 del cotizador y 89 de la landing en verde;
12/12 cotizaciones identicas antes y despues del cambio de reglas; prueba en
vivo del API con alta, modelos, inventario, reglas, micrositio y baja (30/30).
2026-09-21 09:52:47 +00:00

254 lines
9.6 KiB
TypeScript

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<typeof vi.fn> },
desarrollo: undefined as undefined | { findMany: ReturnType<typeof vi.fn> },
}))
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, unknown> = {}): string {
return JSON.stringify({
...params,
...extras,
propuestas: params.propuestas.map((p) => ({ ...p, activa: true })),
})
}
function usarModeloPrisma(): ReturnType<typeof vi.fn> {
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<string, unknown> = {}) {
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<string, unknown>[], tipo: string): Record<string, unknown> {
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<string, unknown>[]
}
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<string, unknown>
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()
})
})