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).
This commit is contained in:
@@ -33,12 +33,17 @@ const prismaMock = vi.hoisted(() => ({
|
||||
auditoriaLog: {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
// Reglas comerciales configurables por el admin (vacías = constantes)
|
||||
reglaCotizadorDesarrollo: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
import { alertasInventarioEndpoint, crearCotizacion } from './cotizaciones.controller.js'
|
||||
import { DESARROLLOS } from '../services/cotizacion.service.js'
|
||||
|
||||
/**
|
||||
* Cotizaciones 100% locales: la unidad sale del espejo (tabla Unidad), la
|
||||
@@ -91,6 +96,7 @@ beforeEach(() => {
|
||||
prismaMock.actividadLocal.create.mockImplementation(async ({ data }) => ({ id: 'act-1', ...data }))
|
||||
prismaMock.automatizacionState.findUnique.mockResolvedValue(null)
|
||||
prismaMock.automatizacionState.upsert.mockResolvedValue({})
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue([])
|
||||
prismaMock.$transaction.mockImplementation(async (ops: Promise<unknown>[]) => Promise.all(ops))
|
||||
})
|
||||
|
||||
@@ -233,6 +239,107 @@ describe('crearCotizacion (POST /api/cotizaciones) — local', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('crearCotizacion — reglas comerciales del admin', () => {
|
||||
/** Reglas guardadas del desarrollo, con los ajustes del test */
|
||||
function reglasGuardadas(
|
||||
desarrollo: string,
|
||||
ajustes: { propuestaInactiva?: string; activoEnCotizador?: boolean; descuentoContado?: number } = {},
|
||||
) {
|
||||
const base = DESARROLLOS[desarrollo]!
|
||||
return [
|
||||
{
|
||||
desarrolloId: desarrollo,
|
||||
parametrosJson: JSON.stringify({
|
||||
...base,
|
||||
...(ajustes.activoEnCotizador !== undefined
|
||||
? { activoEnCotizador: ajustes.activoEnCotizador }
|
||||
: {}),
|
||||
propuestas: base.propuestas.map((p) => ({
|
||||
...p,
|
||||
activa: p.tipo !== ajustes.propuestaInactiva,
|
||||
...(p.tipo === 'CONTADO' && ajustes.descuentoContado !== undefined
|
||||
? { descuentoDefault: ajustes.descuentoContado }
|
||||
: {}),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
it('sin reglas guardadas cotiza igual que siempre (respaldo en constantes)', async () => {
|
||||
const { res, status } = resFake()
|
||||
|
||||
await crearCotizacion(reqFake(bodyBase), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ desarrollo: 'DIAGONAL', descuento: 0 }),
|
||||
})
|
||||
})
|
||||
|
||||
it('usa el descuento configurado por el admin', async () => {
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue(
|
||||
reglasGuardadas('DIAGONAL', { descuentoContado: 0.1 }),
|
||||
)
|
||||
const { res, status } = resFake()
|
||||
|
||||
await crearCotizacion(reqFake({ ...bodyBase, tipoPropuesta: 'CONTADO' }), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ descuento: 0.1, precioFinal: 900_000 }),
|
||||
})
|
||||
})
|
||||
|
||||
it('propuesta desactivada → 409 con el mensaje del servidor y sin persistir', async () => {
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue(
|
||||
reglasGuardadas('DIAGONAL', { propuestaInactiva: 'CREDITO_BANCARIO' }),
|
||||
)
|
||||
const { res } = resFake()
|
||||
|
||||
await expect(crearCotizacion(reqFake(bodyBase), res)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
message: expect.stringContaining('La propuesta BANCARIO no está disponible'),
|
||||
})
|
||||
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||
|
||||
// Las otras propuestas siguen cotizándose
|
||||
const { res: res2, status } = resFake()
|
||||
await crearCotizacion(reqFake({ ...bodyBase, tipoPropuesta: 'CONTADO' }), res2)
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
})
|
||||
|
||||
it('desarrollo desactivado (activoEnCotizador: false) → 409 y sin persistir', async () => {
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue(
|
||||
reglasGuardadas('DIAGONAL', { activoEnCotizador: false }),
|
||||
)
|
||||
const { res } = resFake()
|
||||
|
||||
await expect(crearCotizacion(reqFake(bodyBase), res)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
message: expect.stringContaining('no está disponible en el cotizador'),
|
||||
})
|
||||
expect(prismaMock.cotizacion.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('JSON corrupto en la BD: cotiza con las constantes (un warning, sin fallar)', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue([
|
||||
{ desarrolloId: 'DIAGONAL', parametrosJson: '{corrupto' },
|
||||
])
|
||||
const { res, status } = resFake()
|
||||
|
||||
await crearCotizacion(reqFake(bodyBase), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(prismaMock.cotizacion.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ descuento: 0, precioLista: 1_000_000 }),
|
||||
})
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('JSON inválido'))
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('alertasInventarioEndpoint (POST /api/cotizaciones/alertas-inventario) — actividades locales', () => {
|
||||
it('crea una ActividadLocal al vendedor de cada cotización reciente, con dedup', async () => {
|
||||
prismaMock.cotizacion.findMany.mockResolvedValue([{ id: 'cot-9', vendedorId: 'usr-1' }])
|
||||
|
||||
Reference in New Issue
Block a user