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:
2026-09-21 09:52:47 +00:00
parent 2fc80e7f9e
commit d6548999bf
68 changed files with 10016 additions and 66 deletions

View File

@@ -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' }])