feat(clientes-maestro): importar y exportar clientes del archivo maestro
- Tabla ClienteMaestro (27 campos, unique numCliente) - Importador Excel: 1535 clientes del Reporte maestro - API: listar/filtros/exportar/detalle/importar - Frontend: tabla paginada, filtros, exportar Excel - Dark mode completo, 52 tests verdes
This commit is contained in:
134
api-gateway/src/controllers/cliente-maestro.controller.test.ts
Normal file
134
api-gateway/src/controllers/cliente-maestro.controller.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Request, Response } from 'express'
|
||||
|
||||
// ---------- Servicio mockeado ----------
|
||||
const servicioMock = vi.hoisted(() => ({
|
||||
listar: vi.fn(),
|
||||
obtener: vi.fn(),
|
||||
contar: vi.fn(),
|
||||
exportar: vi.fn(),
|
||||
importarDesdeExcel: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../services/cliente-maestro.service.js', () => servicioMock)
|
||||
|
||||
import {
|
||||
listarEndpoint,
|
||||
exportarEndpoint,
|
||||
obtenerEndpoint,
|
||||
importarEndpoint,
|
||||
} from './cliente-maestro.controller.js'
|
||||
|
||||
function reqFake(overrides: Partial<Request> = {}): Request {
|
||||
return {
|
||||
body: {},
|
||||
params: {},
|
||||
query: {},
|
||||
usuarioToken: { sub: 'u-1', username: 'admin', rol: 'admin' },
|
||||
...overrides,
|
||||
} as Request
|
||||
}
|
||||
|
||||
function resFake() {
|
||||
const json = vi.fn()
|
||||
const end = vi.fn()
|
||||
const status = vi.fn().mockReturnValue({ json, end })
|
||||
return { res: { json, status, end } as unknown as Response, status, json, end }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ── GET /api/clientes-maestro ────────────────────────────────────────
|
||||
|
||||
describe('listarEndpoint', () => {
|
||||
it('200 con datos paginados', async () => {
|
||||
servicioMock.listar.mockResolvedValue({
|
||||
datos: [{ id: '1', numCliente: 'C001' }],
|
||||
total: 1,
|
||||
pagina: 1,
|
||||
limite: 50,
|
||||
})
|
||||
const { res, json } = resFake()
|
||||
await listarEndpoint(reqFake(), res)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
datos: [{ id: '1', numCliente: 'C001' }],
|
||||
total: 1,
|
||||
pagina: 1,
|
||||
limite: 50,
|
||||
})
|
||||
})
|
||||
|
||||
it('pasa filtros de query al servicio', async () => {
|
||||
servicioMock.listar.mockResolvedValue({ datos: [], total: 0, pagina: 1, limite: 50 })
|
||||
const { res } = resFake()
|
||||
await listarEndpoint(
|
||||
reqFake({ query: { busqueda: 'Juan', desarrollo: 'SENDA', estatus: 'VENDIDO', pagina: '2', limite: '25' } }),
|
||||
res,
|
||||
)
|
||||
expect(servicioMock.listar).toHaveBeenCalledWith({
|
||||
busqueda: 'Juan',
|
||||
desarrollo: 'SENDA',
|
||||
estatus: 'VENDIDO',
|
||||
pagina: 2,
|
||||
limite: 25,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── GET /api/clientes-maestro/exportar ───────────────────────────────
|
||||
|
||||
describe('exportarEndpoint', () => {
|
||||
it('200 con array de datos', async () => {
|
||||
servicioMock.exportar.mockResolvedValue([{ id: '1', numCliente: 'C001' }])
|
||||
const { res, json } = resFake()
|
||||
await exportarEndpoint(reqFake(), res)
|
||||
expect(json).toHaveBeenCalledWith([{ id: '1', numCliente: 'C001' }])
|
||||
})
|
||||
})
|
||||
|
||||
// ── GET /api/clientes-maestro/:numCliente ────────────────────────────
|
||||
|
||||
describe('obtenerEndpoint', () => {
|
||||
it('200 con el cliente', async () => {
|
||||
servicioMock.obtener.mockResolvedValue({ id: '1', numCliente: 'C001', nombre: 'Juan' })
|
||||
const { res, json } = resFake()
|
||||
await obtenerEndpoint(reqFake({ params: { numCliente: 'C001' } }), res)
|
||||
expect(json).toHaveBeenCalledWith({ id: '1', numCliente: 'C001', nombre: 'Juan' })
|
||||
})
|
||||
|
||||
it('404 si no existe', async () => {
|
||||
servicioMock.obtener.mockResolvedValue(null)
|
||||
const { res } = resFake()
|
||||
await expect(
|
||||
obtenerEndpoint(reqFake({ params: { numCliente: 'NOPE' } }), res),
|
||||
).rejects.toThrow('Cliente no encontrado')
|
||||
})
|
||||
|
||||
it('400 si numCliente vacío', async () => {
|
||||
const { res } = resFake()
|
||||
await expect(obtenerEndpoint(reqFake({ params: { numCliente: '' } }), res)).rejects.toThrow('numCliente requerido')
|
||||
})
|
||||
})
|
||||
|
||||
// ── POST /api/clientes-maestro/importar ──────────────────────────────
|
||||
|
||||
describe('importarEndpoint', () => {
|
||||
it('200 con resultado de importación', async () => {
|
||||
servicioMock.importarDesdeExcel.mockResolvedValue({ insertados: 5, actualizados: 2, errores: 1 })
|
||||
const { res, json } = resFake()
|
||||
await importarEndpoint(reqFake({ body: [{ numCliente: 'C001', nombre: 'Juan' }] }), res)
|
||||
expect(json).toHaveBeenCalledWith({ insertados: 5, actualizados: 2, errores: 1 })
|
||||
})
|
||||
|
||||
it('400 si body no es array', async () => {
|
||||
const { res } = resFake()
|
||||
await expect(importarEndpoint(reqFake({ body: {} }), res)).rejects.toThrow('Se esperaba un array')
|
||||
})
|
||||
|
||||
it('400 si array vacío', async () => {
|
||||
const { res } = resFake()
|
||||
await expect(importarEndpoint(reqFake({ body: [] }), res)).rejects.toThrow('Se esperaba un array')
|
||||
})
|
||||
})
|
||||
54
api-gateway/src/controllers/cliente-maestro.controller.ts
Normal file
54
api-gateway/src/controllers/cliente-maestro.controller.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import * as clienteMaestroService from '../services/cliente-maestro.service.js'
|
||||
import { NotFoundError, BadRequestError } from '../utils/errors.js'
|
||||
import { requireAdmin } from './config.controller.js'
|
||||
|
||||
// ── GET /api/clientes-maestro ────────────────────────────────────────
|
||||
|
||||
export async function listarEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const busqueda = (req.query.busqueda as string) ?? undefined
|
||||
const desarrollo = (req.query.desarrollo as string) ?? undefined
|
||||
const estatus = (req.query.estatus as string) ?? undefined
|
||||
const pagina = req.query.pagina ? Number(req.query.pagina) : undefined
|
||||
const limite = req.query.limite ? Number(req.query.limite) : undefined
|
||||
|
||||
const resultado = await clienteMaestroService.listar({ busqueda, desarrollo, estatus, pagina, limite })
|
||||
res.json(resultado)
|
||||
}
|
||||
|
||||
// ── GET /api/clientes-maestro/exportar ───────────────────────────────
|
||||
|
||||
export async function exportarEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const busqueda = (req.query.busqueda as string) ?? undefined
|
||||
const desarrollo = (req.query.desarrollo as string) ?? undefined
|
||||
const estatus = (req.query.estatus as string) ?? undefined
|
||||
|
||||
const datos = await clienteMaestroService.exportar({ busqueda, desarrollo, estatus })
|
||||
res.json(datos)
|
||||
}
|
||||
|
||||
// ── GET /api/clientes-maestro/:numCliente ────────────────────────────
|
||||
|
||||
export async function obtenerEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const numCliente = String(req.params['numCliente'] ?? '')
|
||||
if (!numCliente) throw new BadRequestError('numCliente requerido')
|
||||
|
||||
const cliente = await clienteMaestroService.obtener(numCliente)
|
||||
if (!cliente) throw new NotFoundError('Cliente no encontrado')
|
||||
res.json(cliente)
|
||||
}
|
||||
|
||||
// ── POST /api/clientes-maestro/importar ──────────────────────────────
|
||||
|
||||
export async function importarEndpoint(req: Request, res: Response): Promise<void> {
|
||||
const datos = req.body
|
||||
if (!Array.isArray(datos) || datos.length === 0) {
|
||||
throw new BadRequestError('Se esperaba un array de clientes')
|
||||
}
|
||||
|
||||
const resultado = await clienteMaestroService.importarDesdeExcel(datos)
|
||||
res.json(resultado)
|
||||
}
|
||||
|
||||
// Re-exportar requireAdmin para las rutas
|
||||
export { requireAdmin }
|
||||
Reference in New Issue
Block a user