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' }])
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { z } from 'zod'
|
||||
import { getUnidadLocal } from '../services/unidades-local.service.js'
|
||||
import { calcularPropuesta, obtenerPrecioLista } from '../services/cotizacion.service.js'
|
||||
import {
|
||||
calcularPropuesta,
|
||||
obtenerPrecioLista,
|
||||
propuestasVigentes,
|
||||
TIPO_PROPUESTA_API,
|
||||
} from '../services/cotizacion.service.js'
|
||||
import { getReglasDesarrollo } from '../services/reglas-desarrollo.service.js'
|
||||
import {
|
||||
procesarAlertasInventario,
|
||||
type ResultadoAlertas,
|
||||
@@ -76,6 +82,24 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
|
||||
// el mismo inmueble ya apartado/vendido
|
||||
validarUnidadDisponible(unidad)
|
||||
|
||||
// Reglas comerciales del desarrollo: las configuradas por el admin (BD) con
|
||||
// respaldo en las constantes históricas. Un desarrollo o una propuesta
|
||||
// desactivados se rechazan con el mensaje que muestra el cotizador.
|
||||
const reglas = await getReglasDesarrollo(unidad.desarrollo)
|
||||
if (reglas.activoEnCotizador === false) {
|
||||
throw new AppError(
|
||||
409,
|
||||
`El desarrollo ${reglas.nombre ?? reglas.id} no está disponible en el cotizador. Elige una unidad de otro desarrollo.`,
|
||||
)
|
||||
}
|
||||
const tipoInterno = TIPO_PROPUESTA_API[body.tipoPropuesta]
|
||||
if (!propuestasVigentes(reglas).some((p) => p.tipo === tipoInterno)) {
|
||||
throw new AppError(
|
||||
409,
|
||||
`La propuesta ${body.tipoPropuesta} no está disponible para el desarrollo ${reglas.id}. Elige otra propuesta.`,
|
||||
)
|
||||
}
|
||||
|
||||
const apartado = body.apartado ?? 20000 // APARTADO_DEFAULT
|
||||
const propuesta = calcularPropuesta(
|
||||
unidad,
|
||||
@@ -85,6 +109,7 @@ export async function crearCotizacion(req: Request, res: Response): Promise<void
|
||||
body.descuento,
|
||||
body.enganchePct,
|
||||
body.numMensualidades,
|
||||
reglas,
|
||||
)
|
||||
const precioLista = obtenerPrecioLista(unidad, body.acabados)
|
||||
|
||||
|
||||
524
api-gateway/src/controllers/desarrollos.controller.test.ts
Normal file
524
api-gateway/src/controllers/desarrollos.controller.test.ts
Normal file
@@ -0,0 +1,524 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
// ---------- Prisma mockeado (el servicio corre de verdad) ----------
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
desarrollo: {
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
modeloDesarrollo: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
reglaCotizadorDesarrollo: {
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
upsert: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
unidad: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
create: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
cotizacion: { count: vi.fn(), findMany: vi.fn() },
|
||||
leadCaptura: { count: vi.fn(), findMany: vi.fn() },
|
||||
cliente: { count: vi.fn(), findMany: vi.fn() },
|
||||
planPago: { count: vi.fn(), findMany: vi.fn() },
|
||||
auditoriaLog: { create: vi.fn().mockResolvedValue({}) },
|
||||
$transaction: vi.fn(async (ops: Promise<unknown>[]) => Promise.all(ops)),
|
||||
}))
|
||||
|
||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
import { requireAdminEstricto } from './config.controller.js'
|
||||
import {
|
||||
actualizarDesarrolloEndpoint,
|
||||
agregarModeloEndpoint,
|
||||
crearDesarrolloEndpoint,
|
||||
crearUnidadesEndpoint,
|
||||
eliminarDesarrolloEndpoint,
|
||||
eliminarUnidadEndpoint,
|
||||
guardarReglasEndpoint,
|
||||
listarDesarrollosEndpoint,
|
||||
listarDesarrollosPublicosEndpoint,
|
||||
obtenerDesarrolloPublicoEndpoint,
|
||||
} from './desarrollos.controller.js'
|
||||
|
||||
function reqFake(opts: { body?: unknown; params?: Record<string, string>; query?: Record<string, string>; token?: { sub: string; rol: string } }): Request {
|
||||
return {
|
||||
body: opts.body ?? {},
|
||||
params: opts.params ?? {},
|
||||
query: opts.query ?? {},
|
||||
usuarioToken: opts.token,
|
||||
} as unknown as Request
|
||||
}
|
||||
|
||||
function resFake() {
|
||||
const json = vi.fn()
|
||||
const end = vi.fn()
|
||||
const statusFn = vi.fn().mockReturnValue({ json, end })
|
||||
return { res: { json, end, status: statusFn } as unknown as Response, status: statusFn, json, end }
|
||||
}
|
||||
|
||||
const ADMIN = { sub: 'u-admin', rol: 'admin' }
|
||||
|
||||
function filaDesarrollo(over: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'd-1',
|
||||
clave: 'SENDA',
|
||||
nombre: 'Senda',
|
||||
estado: 'ACTIVO',
|
||||
tipo: 'VERTICAL',
|
||||
constructora: null,
|
||||
direccion: null,
|
||||
zona: 'Zapopan',
|
||||
descripcion: null,
|
||||
amenidades: null,
|
||||
fechaInicioObra: null,
|
||||
fechaEntregaProgramada: null,
|
||||
fechaEntregaReprogramada: null,
|
||||
activoEnCotizador: true,
|
||||
activoEnAlta: true,
|
||||
prefijoUnidadPdf: 'CASA',
|
||||
imagenUrl: null,
|
||||
logoUrl: null,
|
||||
createdAt: new Date('2026-09-21T00:00:00.000Z'),
|
||||
updatedAt: new Date('2026-09-21T00:00:00.000Z'),
|
||||
modelos: [],
|
||||
reglas: null,
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
function filaUnidad(over: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 'u-1',
|
||||
codigo: 'SENDA-1',
|
||||
desarrollo: 'SENDA',
|
||||
modelo: 'HIMALAYA',
|
||||
manzana: '',
|
||||
lote: '',
|
||||
nivel: '',
|
||||
supConstruccion: 120,
|
||||
supExcedente: 0,
|
||||
supEstacionamiento: 1,
|
||||
precioLista: 3000000,
|
||||
precioAcabados: 3200000,
|
||||
status: 'DISPONIBLE',
|
||||
etapa: '',
|
||||
updatedAt: new Date('2026-09-21T00:00:00.000Z'),
|
||||
...over,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prismaMock.unidad.findMany.mockResolvedValue([])
|
||||
prismaMock.unidad.count.mockResolvedValue(0)
|
||||
prismaMock.cotizacion.count.mockResolvedValue(0)
|
||||
prismaMock.leadCaptura.count.mockResolvedValue(0)
|
||||
prismaMock.cliente.count.mockResolvedValue(0)
|
||||
prismaMock.planPago.count.mockResolvedValue(0)
|
||||
prismaMock.unidad.create.mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({
|
||||
id: `id-${String(data['codigo'])}`,
|
||||
updatedAt: new Date('2026-09-21T00:00:00.000Z'),
|
||||
...data,
|
||||
}))
|
||||
})
|
||||
|
||||
// ── Permisos ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('requireAdminEstricto', () => {
|
||||
it('deja pasar al admin', () => {
|
||||
const next = vi.fn()
|
||||
requireAdminEstricto(reqFake({ token: ADMIN }), {} as Response, next)
|
||||
expect(next).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('403 para manager y para vendedor', () => {
|
||||
for (const rol of ['manager', 'vendedor', 'coordinador', 'corporativo']) {
|
||||
expect(() => requireAdminEstricto(reqFake({ token: { sub: 'u-1', rol } }), {} as Response, vi.fn())).toThrow(
|
||||
'Solo un administrador puede hacer esta operación',
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('403 sin token', () => {
|
||||
expect(() => requireAdminEstricto(reqFake({}), {} as Response, vi.fn())).toThrow()
|
||||
})
|
||||
|
||||
it('los endpoints de escritura quedan detrás del guard (403 con rol manager)', async () => {
|
||||
// El guard se monta en las rutas; aquí se comprueba que corta antes del servicio
|
||||
const req = reqFake({ token: { sub: 'u-1', rol: 'manager' }, body: { clave: 'NUEVO', nombre: 'Nuevo' } })
|
||||
expect(() => requireAdminEstricto(req, {} as Response, vi.fn())).toThrow()
|
||||
expect(prismaMock.desarrollo.create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Fichas ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/desarrollos', () => {
|
||||
it('201 con la ficha cuando la clave es nueva', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValueOnce(null).mockResolvedValueOnce(filaDesarrollo())
|
||||
prismaMock.desarrollo.create.mockResolvedValue({ id: 'd-1' })
|
||||
const { res, status, json } = resFake()
|
||||
|
||||
await crearDesarrolloEndpoint(reqFake({ body: { clave: 'SENDA', nombre: 'Senda' }, token: ADMIN }), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(json).toHaveBeenCalledWith(expect.objectContaining({ clave: 'SENDA' }))
|
||||
expect(prismaMock.auditoriaLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ accion: 'CREAR_DESARROLLO', entidad: 'desarrollo', usuarioId: 'u-admin' }),
|
||||
})
|
||||
})
|
||||
|
||||
it('409 con clave duplicada y sin crear', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-existente' })
|
||||
const { res } = resFake()
|
||||
|
||||
await expect(crearDesarrolloEndpoint(reqFake({ body: { clave: 'SENDA', nombre: 'Senda' }, token: ADMIN }), res)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
})
|
||||
expect(prismaMock.desarrollo.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('400 si la clave no es mayúsculas sin espacios', async () => {
|
||||
const { res } = resFake()
|
||||
await expect(
|
||||
crearDesarrolloEndpoint(reqFake({ body: { clave: 'senda nueva', nombre: 'Senda' }, token: ADMIN }), res),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/desarrollos y detalle', () => {
|
||||
it('devuelve el contrato del listado', async () => {
|
||||
prismaMock.desarrollo.findMany.mockResolvedValue([
|
||||
filaDesarrollo({
|
||||
modelos: [{ id: 'm-1', desarrolloId: 'd-1', nombre: 'HIMALAYA', recamaras: 3, banos: 2.5, estacionamientos: 2, supConstruccion: 120, activo: true }],
|
||||
reglas: { desarrolloId: 'd-1', parametrosJson: '{}', updatedAt: new Date(), updatedPorId: null },
|
||||
}),
|
||||
])
|
||||
prismaMock.unidad.findMany.mockResolvedValue([
|
||||
{ desarrollo: 'SENDA', status: 'DISPONIBLE' },
|
||||
{ desarrollo: 'SENDA', status: 'APARTADO' },
|
||||
])
|
||||
const { res, json } = resFake()
|
||||
|
||||
await listarDesarrollosEndpoint(reqFake({ token: ADMIN }), res)
|
||||
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
desarrollos: [
|
||||
expect.objectContaining({
|
||||
clave: 'SENDA',
|
||||
unidadesTotales: 2,
|
||||
unidadesDisponibles: 1,
|
||||
tieneReglas: true,
|
||||
modelos: [
|
||||
{ id: 'm-1', nombre: 'HIMALAYA', recamaras: 3, banos: 2.5, estacionamientos: 2, supConstruccion: 120, activo: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('PATCH /api/desarrollos/:id', () => {
|
||||
it('404 si no existe', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue(null)
|
||||
await expect(actualizarDesarrolloEndpoint(reqFake({ params: { id: 'nope' }, body: { zona: 'X' }, token: ADMIN }), resFake().res)).rejects.toMatchObject({
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it('actualiza solo los campos enviados', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValueOnce({ id: 'd-1', clave: 'SENDA' }).mockResolvedValueOnce(filaDesarrollo({ zona: 'Tlajomulco' }))
|
||||
prismaMock.desarrollo.update.mockResolvedValue(filaDesarrollo())
|
||||
const { res, json } = resFake()
|
||||
|
||||
await actualizarDesarrolloEndpoint(reqFake({ params: { id: 'd-1' }, body: { zona: 'Tlajomulco' }, token: ADMIN }), res)
|
||||
|
||||
expect(prismaMock.desarrollo.update).toHaveBeenCalledWith({ where: { id: 'd-1' }, data: { zona: 'Tlajomulco' } })
|
||||
expect(json).toHaveBeenCalledWith(expect.objectContaining({ zona: 'Tlajomulco' }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/desarrollos/:id', () => {
|
||||
it('409 con inventario y operación; 204 sin dependencias', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
prismaMock.unidad.findMany.mockResolvedValue([{ codigo: 'SENDA-1' }, { codigo: 'SENDA-2' }])
|
||||
prismaMock.cotizacion.count.mockResolvedValue(3)
|
||||
|
||||
await expect(eliminarDesarrolloEndpoint(reqFake({ params: { id: 'd-1' }, token: ADMIN }), resFake().res)).rejects.toMatchObject({
|
||||
status: 409,
|
||||
message: 'El desarrollo tiene 2 unidades y 3 registros de operación; desactívalo en vez de borrarlo',
|
||||
})
|
||||
expect(prismaMock.desarrollo.delete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('204 y auditoría cuando el desarrollo está limpio', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
const { res, status } = resFake()
|
||||
|
||||
await eliminarDesarrolloEndpoint(reqFake({ params: { id: 'd-1' }, token: ADMIN }), res)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(204)
|
||||
expect(prismaMock.desarrollo.delete).toHaveBeenCalledWith({ where: { id: 'd-1' } })
|
||||
expect(prismaMock.auditoriaLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ accion: 'ELIMINAR_DESARROLLO', entidadId: 'd-1' }),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── Modelos y reglas ──────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/desarrollos/:id/modelos', () => {
|
||||
it('201 con el modelo creado', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1' })
|
||||
prismaMock.modeloDesarrollo.findUnique.mockResolvedValue(null)
|
||||
prismaMock.modeloDesarrollo.create.mockResolvedValue({ id: 'm-1', desarrolloId: 'd-1', nombre: 'PANORAMA' })
|
||||
const { res, status } = resFake()
|
||||
|
||||
await agregarModeloEndpoint(reqFake({ params: { id: 'd-1' }, body: { nombre: 'PANORAMA', recamaras: 3 }, token: ADMIN }), res)
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /api/desarrollos/:id/reglas', () => {
|
||||
const reglas = {
|
||||
precioUnico: false,
|
||||
fuenteDeN: 'fechaEntrega',
|
||||
nDefault: 13,
|
||||
cuotaMantenimiento: 1500,
|
||||
prefijoUnidadPdf: 'CASA',
|
||||
propuestas: [
|
||||
{
|
||||
tipo: 'CREDITO_BANCARIO',
|
||||
claveDescuento: 'creditoBancario',
|
||||
titulo: 'Crédito Bancario',
|
||||
descuentoDefault: 0,
|
||||
pctEnganche: 0.025,
|
||||
engancheBase: 'lista',
|
||||
pctPagoFinal: 0.9,
|
||||
pagoFinalBase: 'final',
|
||||
modoMensualidades: 'remanente',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
it('guarda y devuelve las reglas parseadas', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1' })
|
||||
prismaMock.reglaCotizadorDesarrollo.upsert.mockResolvedValue({
|
||||
desarrolloId: 'd-1',
|
||||
parametrosJson: JSON.stringify(reglas),
|
||||
updatedAt: new Date('2026-09-21T00:00:00.000Z'),
|
||||
updatedPorId: 'u-admin',
|
||||
})
|
||||
const { res, json } = resFake()
|
||||
|
||||
await guardarReglasEndpoint(reqFake({ params: { id: 'd-1' }, body: reglas, token: ADMIN }), res)
|
||||
expect(json).toHaveBeenCalledWith(expect.objectContaining({ desarrolloId: 'd-1', parametros: expect.objectContaining({ nDefault: 13 }) }))
|
||||
})
|
||||
|
||||
it('400 si un porcentaje se sale de 0..1', async () => {
|
||||
const malas = { ...reglas, propuestas: [{ ...reglas.propuestas[0], pctPagoFinal: 1.2 }] }
|
||||
await expect(guardarReglasEndpoint(reqFake({ params: { id: 'd-1' }, body: malas, token: ADMIN }), resFake().res)).rejects.toThrow()
|
||||
expect(prismaMock.reglaCotizadorDesarrollo.upsert).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ── Inventario ────────────────────────────────────────────────────────
|
||||
|
||||
describe('POST /api/desarrollos/:id/unidades', () => {
|
||||
it('genera el rango con {clave} y {n}: 201 con las creadas', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
const { res, status, json } = resFake()
|
||||
|
||||
await crearUnidadesEndpoint(
|
||||
reqFake({
|
||||
params: { id: 'd-1' },
|
||||
body: { generar: { patron: 'SENDA-401-{n}', desde: 1, hasta: 3, precioLista: 2000000 } },
|
||||
token: ADMIN,
|
||||
}),
|
||||
res,
|
||||
)
|
||||
|
||||
expect(status).toHaveBeenCalledWith(201)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
creadas: 3,
|
||||
unidades: expect.arrayContaining([expect.objectContaining({ codigo: 'SENDA-401-1', status: 'DISPONIBLE' })]),
|
||||
})
|
||||
expect(prismaMock.auditoriaLog.create).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ accion: 'CREAR_UNIDADES', entidad: 'unidad' }),
|
||||
})
|
||||
})
|
||||
|
||||
it('{clave} se sustituye por la clave del desarrollo', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
const { json } = resFake()
|
||||
|
||||
await crearUnidadesEndpoint(
|
||||
reqFake({ params: { id: 'd-1' }, body: { generar: { patron: '{clave}-{n}', desde: 7, hasta: 8, precioLista: 1 } }, token: ADMIN }),
|
||||
resFake().res,
|
||||
)
|
||||
void json
|
||||
const creadas = prismaMock.unidad.create.mock.calls.map((c) => (c[0] as { data: { codigo: string } }).data.codigo)
|
||||
expect(creadas).toEqual(['SENDA-7', 'SENDA-8'])
|
||||
})
|
||||
|
||||
it('409 sin crear nada si algún código ya existe', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
prismaMock.unidad.findMany.mockResolvedValueOnce([]).mockResolvedValueOnce([{ codigo: 'SENDA-5' }])
|
||||
const { res } = resFake()
|
||||
|
||||
await expect(
|
||||
crearUnidadesEndpoint(
|
||||
reqFake({
|
||||
params: { id: 'd-1' },
|
||||
body: {
|
||||
unidades: [
|
||||
{ codigo: 'SENDA-5', precioLista: 1 },
|
||||
{ codigo: 'SENDA-6', precioLista: 1 },
|
||||
],
|
||||
},
|
||||
token: ADMIN,
|
||||
}),
|
||||
res,
|
||||
),
|
||||
).rejects.toMatchObject({ status: 409 })
|
||||
expect(prismaMock.unidad.create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('409 si el mismo código se repite en la alta', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
prismaMock.unidad.findMany.mockResolvedValue([])
|
||||
|
||||
await expect(
|
||||
crearUnidadesEndpoint(
|
||||
reqFake({
|
||||
params: { id: 'd-1' },
|
||||
body: { unidades: [{ codigo: 'SENDA-1', precioLista: 1 }, { codigo: 'SENDA-1', precioLista: 2 }] },
|
||||
token: ADMIN,
|
||||
}),
|
||||
resFake().res,
|
||||
),
|
||||
).rejects.toMatchObject({ status: 409 })
|
||||
})
|
||||
|
||||
it('404 si el desarrollo no existe', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue(null)
|
||||
await expect(
|
||||
crearUnidadesEndpoint(reqFake({ params: { id: 'nope' }, body: { generar: { patron: '{n}', desde: 1, hasta: 1, precioLista: 1 } }, token: ADMIN }), resFake().res),
|
||||
).rejects.toMatchObject({ status: 404 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /api/desarrollos/:id/unidades/:codigo', () => {
|
||||
it('409 si la unidad tiene cotizaciones o ventas', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
prismaMock.unidad.findUnique.mockResolvedValue(filaUnidad())
|
||||
prismaMock.planPago.count.mockResolvedValue(1)
|
||||
|
||||
await expect(
|
||||
eliminarUnidadEndpoint(reqFake({ params: { id: 'd-1', codigo: 'SENDA-1' }, token: ADMIN }), resFake().res),
|
||||
).rejects.toMatchObject({ status: 409 })
|
||||
expect(prismaMock.unidad.delete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('204 si la unidad está libre', async () => {
|
||||
prismaMock.desarrollo.findUnique.mockResolvedValue({ id: 'd-1', clave: 'SENDA' })
|
||||
prismaMock.unidad.findUnique.mockResolvedValue(filaUnidad())
|
||||
const { res, status } = resFake()
|
||||
|
||||
await eliminarUnidadEndpoint(reqFake({ params: { id: 'd-1', codigo: 'SENDA-1' }, token: ADMIN }), res)
|
||||
expect(status).toHaveBeenCalledWith(204)
|
||||
})
|
||||
})
|
||||
|
||||
// ── Público (sin auth) ────────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/publico/desarrollos', () => {
|
||||
it('publica solo el desarrollo activo con inventario disponible y sin datos personales', async () => {
|
||||
prismaMock.desarrollo.findMany.mockResolvedValue([
|
||||
filaDesarrollo({
|
||||
modelos: [{ id: 'm-1', desarrolloId: 'd-1', nombre: 'HIMALAYA', recamaras: 3, banos: 2.5, estacionamientos: 2, supConstruccion: 120, activo: true }],
|
||||
}),
|
||||
])
|
||||
prismaMock.unidad.findMany.mockResolvedValue([
|
||||
filaUnidad({ codigo: 'SENDA-1', nivel: '3' }),
|
||||
filaUnidad({ codigo: 'SENDA-2', status: 'VENDIDO', precioLista: 5000000 }),
|
||||
])
|
||||
const { res, json } = resFake()
|
||||
|
||||
await listarDesarrollosPublicosEndpoint(reqFake({}), res)
|
||||
|
||||
const payload = (json.mock.calls[0] as [ { desarrollos: Record<string, unknown>[] } ])[0]
|
||||
const desarrollo = payload.desarrollos[0] as Record<string, unknown>
|
||||
|
||||
expect(prismaMock.desarrollo.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { activoEnCotizador: true, estado: { not: 'CERRADO' } } }),
|
||||
)
|
||||
expect(Object.keys(desarrollo).sort()).toEqual(
|
||||
[
|
||||
'amenidades',
|
||||
'clave',
|
||||
'descripcion',
|
||||
'direccion',
|
||||
'estado',
|
||||
'fechaEntregaProgramada',
|
||||
'imagenUrl',
|
||||
'logoUrl',
|
||||
'modelos',
|
||||
'nombre',
|
||||
'tipo',
|
||||
'unidadesDisponibles',
|
||||
'zona',
|
||||
].sort(),
|
||||
)
|
||||
expect(desarrollo['unidadesDisponibles']).toEqual([
|
||||
{ codigo: 'SENDA-1', modelo: 'HIMALAYA', nivel: '3', precioLista: 3000000, precioAcabados: 3200000, supConstruccion: 120 },
|
||||
])
|
||||
|
||||
// El camino público nunca consulta clientes, cotizaciones ni planes
|
||||
expect(prismaMock.cliente.count).not.toHaveBeenCalled()
|
||||
expect(prismaMock.cliente.findMany).not.toHaveBeenCalled()
|
||||
expect(prismaMock.cotizacion.count).not.toHaveBeenCalled()
|
||||
expect(prismaMock.planPago.count).not.toHaveBeenCalled()
|
||||
expect(prismaMock.leadCaptura.count).not.toHaveBeenCalled()
|
||||
for (const prohibido of ['cliente', 'email', 'telefono', 'curp', 'vendedor', 'apartado', 'rfc']) {
|
||||
expect(JSON.stringify(payload).toLowerCase()).not.toContain(prohibido)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/publico/desarrollos/:clave', () => {
|
||||
it('404 si no existe o no es público', async () => {
|
||||
prismaMock.desarrollo.findFirst.mockResolvedValue(null)
|
||||
await expect(obtenerDesarrolloPublicoEndpoint(reqFake({ params: { clave: 'NOEXISTE' } }), resFake().res)).rejects.toMatchObject({
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it('resuelve la clave sin importar mayúsculas', async () => {
|
||||
prismaMock.desarrollo.findFirst.mockResolvedValue(filaDesarrollo())
|
||||
const { res, json } = resFake()
|
||||
|
||||
await obtenerDesarrolloPublicoEndpoint(reqFake({ params: { clave: 'senda' } }), res)
|
||||
|
||||
expect(prismaMock.desarrollo.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: { clave: 'SENDA', activoEnCotizador: true, estado: { not: 'CERRADO' } } }),
|
||||
)
|
||||
expect(json).toHaveBeenCalledWith(expect.objectContaining({ clave: 'SENDA' }))
|
||||
})
|
||||
})
|
||||
141
api-gateway/src/controllers/desarrollos.controller.ts
Normal file
141
api-gateway/src/controllers/desarrollos.controller.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Endpoints del módulo Desarrollos.
|
||||
*
|
||||
* Todo el CRUD y el inventario son solo admin (`requireAdminEstricto`, montado
|
||||
* en las rutas); el micrositio público lee /api/publico/desarrollos sin auth,
|
||||
* con información exclusivamente comercial (nunca clientes ni cotizaciones).
|
||||
*/
|
||||
import type { Request, Response } from 'express'
|
||||
import * as service from '../services/desarrollos.service.js'
|
||||
import * as auditoria from '../services/auditoria.service.js'
|
||||
import { AppError } from '../utils/errors.js'
|
||||
|
||||
/** Usuario del token para la bitácora (modo local sin auth: 'desconocido') */
|
||||
function usuarioDe(req: Request): string {
|
||||
return req.usuarioToken?.sub ?? 'desconocido'
|
||||
}
|
||||
|
||||
function idDe(req: Request): string {
|
||||
const id = String(req.params['id'] ?? '')
|
||||
if (id === '') throw new AppError(400, 'Falta el parámetro :id')
|
||||
return id
|
||||
}
|
||||
|
||||
// ── Fichas ────────────────────────────────────────────────────────────
|
||||
|
||||
/** GET /api/desarrollos */
|
||||
export async function listarDesarrollosEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
res.json(await service.listarDesarrollos())
|
||||
}
|
||||
|
||||
/** GET /api/desarrollos/:id */
|
||||
export async function obtenerDesarrolloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
res.json(await service.obtenerDesarrollo(idDe(req)))
|
||||
}
|
||||
|
||||
/** POST /api/desarrollos */
|
||||
export async function crearDesarrolloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const body = service.crearDesarrolloSchema.parse(req.body)
|
||||
const ficha = await service.crearDesarrollo(body)
|
||||
res.status(201).json(ficha)
|
||||
auditoria.registrar(usuarioDe(req), 'CREAR_DESARROLLO', 'desarrollo', ficha.id, ficha.clave)
|
||||
}
|
||||
|
||||
/** PATCH /api/desarrollos/:id */
|
||||
export async function actualizarDesarrolloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const body = service.actualizarDesarrolloSchema.parse(req.body)
|
||||
const ficha = await service.actualizarDesarrollo(idDe(req), body)
|
||||
res.json(ficha)
|
||||
auditoria.registrar(
|
||||
usuarioDe(req),
|
||||
'ACTUALIZAR_DESARROLLO',
|
||||
'desarrollo',
|
||||
ficha.id,
|
||||
Object.keys(req.body as Record<string, unknown>).join(', '),
|
||||
)
|
||||
}
|
||||
|
||||
/** DELETE /api/desarrollos/:id — 409 si tiene inventario u operación */
|
||||
export async function eliminarDesarrolloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const id = idDe(req)
|
||||
await service.eliminarDesarrollo(id)
|
||||
res.status(204).end()
|
||||
auditoria.registrar(usuarioDe(req), 'ELIMINAR_DESARROLLO', 'desarrollo', id)
|
||||
}
|
||||
|
||||
// ── Modelos ───────────────────────────────────────────────────────────
|
||||
|
||||
/** POST /api/desarrollos/:id/modelos */
|
||||
export async function agregarModeloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const desarrolloId = idDe(req)
|
||||
const body = service.crearModeloSchema.parse(req.body)
|
||||
const modelo = await service.agregarModelo(desarrolloId, body)
|
||||
res.status(201).json(modelo)
|
||||
auditoria.registrar(usuarioDe(req), 'AGREGAR_MODELO', 'desarrollo', desarrolloId, modelo.nombre)
|
||||
}
|
||||
|
||||
/** PATCH /api/desarrollos/modelos/:modeloId */
|
||||
export async function actualizarModeloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const modeloId = String(req.params['modeloId'])
|
||||
const body = service.actualizarModeloSchema.parse(req.body)
|
||||
const modelo = await service.actualizarModelo(modeloId, body)
|
||||
res.json(modelo)
|
||||
auditoria.registrar(usuarioDe(req), 'ACTUALIZAR_MODELO', 'desarrollo', modelo.desarrolloId, modelo.nombre)
|
||||
}
|
||||
|
||||
/** DELETE /api/desarrollos/modelos/:modeloId — 409 si el modelo tiene unidades */
|
||||
export async function eliminarModeloEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const modeloId = String(req.params['modeloId'])
|
||||
await service.eliminarModelo(modeloId)
|
||||
res.status(204).end()
|
||||
auditoria.registrar(usuarioDe(req), 'ELIMINAR_MODELO', 'desarrollo', modeloId)
|
||||
}
|
||||
|
||||
// ── Reglas del cotizador ──────────────────────────────────────────────
|
||||
|
||||
/** PUT /api/desarrollos/:id/reglas */
|
||||
export async function guardarReglasEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const desarrolloId = idDe(req)
|
||||
const parametros = service.parametrosDesarrolloSchema.parse(req.body)
|
||||
const reglas = await service.guardarReglas(desarrolloId, parametros, req.usuarioToken?.sub ?? null)
|
||||
res.json(reglas)
|
||||
auditoria.registrar(usuarioDe(req), 'GUARDAR_REGLAS_COTIZADOR', 'desarrollo', desarrolloId)
|
||||
}
|
||||
|
||||
// ── Inventario ────────────────────────────────────────────────────────
|
||||
|
||||
/** POST /api/desarrollos/:id/unidades — alta manual o por generador */
|
||||
export async function crearUnidadesEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const desarrolloId = idDe(req)
|
||||
const body = service.crearUnidadesSchema.parse(req.body)
|
||||
const resultado = await service.crearUnidades(desarrolloId, body)
|
||||
res.status(201).json(resultado)
|
||||
auditoria.registrar(
|
||||
usuarioDe(req),
|
||||
'CREAR_UNIDADES',
|
||||
'unidad',
|
||||
desarrolloId,
|
||||
`${resultado.creadas} unidad(es): ${resultado.unidades.map((u) => u.codigo).join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
/** DELETE /api/desarrollos/:id/unidades/:codigo — 409 si la unidad tiene operación */
|
||||
export async function eliminarUnidadEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const desarrolloId = idDe(req)
|
||||
const codigo = String(req.params['codigo'])
|
||||
await service.eliminarUnidad(desarrolloId, codigo)
|
||||
res.status(204).end()
|
||||
auditoria.registrar(usuarioDe(req), 'ELIMINAR_UNIDAD', 'unidad', codigo)
|
||||
}
|
||||
|
||||
// ── Público (micrositio) ──────────────────────────────────────────────
|
||||
|
||||
/** GET /api/publico/desarrollos */
|
||||
export async function listarDesarrollosPublicosEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
res.json(await service.listarDesarrollosPublicos())
|
||||
}
|
||||
|
||||
/** GET /api/publico/desarrollos/:clave */
|
||||
export async function obtenerDesarrolloPublicoEndpoint(req: Request, res: Response): Promise<void> {
|
||||
res.json(await service.obtenerDesarrolloPublico(String(req.params['clave'])))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
const prismaMock = vi.hoisted(() => ({
|
||||
reglaCotizadorDesarrollo: { findMany: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
|
||||
import { reglasCotizadorEndpoint } from './reglas-cotizador.controller.js'
|
||||
import { DESARROLLOS } from '../services/cotizacion.service.js'
|
||||
|
||||
function resFake() {
|
||||
const json = vi.fn()
|
||||
return { res: { json } as unknown as Response, json }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue([])
|
||||
})
|
||||
|
||||
describe('GET /api/desarrollos/reglas-cotizador', () => {
|
||||
it('devuelve el mapa reglasPorDesarrollo con las reglas efectivas', async () => {
|
||||
prismaMock.reglaCotizadorDesarrollo.findMany.mockResolvedValue([
|
||||
{
|
||||
desarrolloId: 'SENDA',
|
||||
parametrosJson: JSON.stringify(DESARROLLOS['SENDA']),
|
||||
activoEnCotizador: false,
|
||||
},
|
||||
])
|
||||
const { res, json } = resFake()
|
||||
|
||||
await reglasCotizadorEndpoint({} as Request, res)
|
||||
|
||||
const cuerpo = json.mock.calls[0]?.[0] as {
|
||||
reglasPorDesarrollo: Record<string, { activoEnCotizador?: boolean; propuestas: unknown[] }>
|
||||
}
|
||||
expect(Object.keys(cuerpo.reglasPorDesarrollo).sort()).toEqual(Object.keys(DESARROLLOS).sort())
|
||||
expect(cuerpo.reglasPorDesarrollo['SENDA']?.activoEnCotizador).toBe(false)
|
||||
expect(cuerpo.reglasPorDesarrollo['SENDA']?.propuestas).toHaveLength(4)
|
||||
// Los desarrollos sin reglas guardadas salen con sus constantes
|
||||
expect(cuerpo.reglasPorDesarrollo['DIAGONAL']).toEqual(DESARROLLOS['DIAGONAL'])
|
||||
})
|
||||
})
|
||||
14
api-gateway/src/controllers/reglas-cotizador.controller.ts
Normal file
14
api-gateway/src/controllers/reglas-cotizador.controller.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { getReglasEfectivas } from '../services/reglas-desarrollo.service.js'
|
||||
|
||||
/**
|
||||
* GET /api/desarrollos/reglas-cotizador — reglas comerciales EFECTIVAS por
|
||||
* desarrollo (las configuradas por el admin en la BD, con respaldo en las
|
||||
* constantes históricas para los desarrollos sin reglas guardadas).
|
||||
*
|
||||
* Autenticado y disponible para cualquier rol que cotice (el middleware de auth
|
||||
* cubre todo /api salvo las rutas públicas). Devuelve el mapa `reglasPorDesarrollo`.
|
||||
*/
|
||||
export async function reglasCotizadorEndpoint(_req: Request, res: Response): Promise<void> {
|
||||
res.json({ reglasPorDesarrollo: await getReglasEfectivas() })
|
||||
}
|
||||
Reference in New Issue
Block a user