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).
254 lines
9.6 KiB
TypeScript
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()
|
|
})
|
|
})
|