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:
2026-09-07 09:11:09 +00:00
parent c2a8f4e929
commit 9021733455
18 changed files with 1428 additions and 10 deletions

View File

@@ -0,0 +1,185 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
// ---------- Prisma mockeado ----------
const prismaMock = vi.hoisted(() => ({
clienteMaestro: {
findMany: vi.fn(),
findUnique: vi.fn(),
count: vi.fn(),
upsert: vi.fn(),
create: vi.fn(),
update: vi.fn(),
},
}))
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
import {
listar,
obtener,
contar,
exportar,
importarDesdeExcel,
} from './cliente-maestro.service.js'
beforeEach(() => {
vi.clearAllMocks()
})
// ── listar ───────────────────────────────────────────────────────────
describe('listar', () => {
it('devuelve datos, total, pagina y limite', async () => {
prismaMock.clienteMaestro.findMany.mockResolvedValue([
{ id: '1', numCliente: 'C001', nombre: 'Juan', apellidoPaterno: 'Perez', importadoEn: new Date() },
])
prismaMock.clienteMaestro.count.mockResolvedValue(1)
const result = await listar({ pagina: 1, limite: 50 })
expect(result.datos).toHaveLength(1)
expect(result.total).toBe(1)
expect(result.pagina).toBe(1)
expect(result.limite).toBe(50)
})
it('aplica filtro de búsqueda con OR', async () => {
prismaMock.clienteMaestro.findMany.mockResolvedValue([])
prismaMock.clienteMaestro.count.mockResolvedValue(0)
await listar({ busqueda: 'Juan', pagina: 1, limite: 50 })
expect(prismaMock.clienteMaestro.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
{ nombre: { contains: 'Juan' } },
]),
}),
}),
)
})
it('aplica filtro de desarrollo', async () => {
prismaMock.clienteMaestro.findMany.mockResolvedValue([])
prismaMock.clienteMaestro.count.mockResolvedValue(0)
await listar({ desarrollo: 'SENDA', pagina: 1, limite: 50 })
expect(prismaMock.clienteMaestro.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ desarrollo: 'SENDA' }),
}),
)
})
it('aplica filtro de estatus', async () => {
prismaMock.clienteMaestro.findMany.mockResolvedValue([])
prismaMock.clienteMaestro.count.mockResolvedValue(0)
await listar({ estatus: 'VENDIDO', pagina: 1, limite: 50 })
expect(prismaMock.clienteMaestro.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({ estatus: 'VENDIDO' }),
}),
)
})
})
// ── obtener ──────────────────────────────────────────────────────────
describe('obtener', () => {
it('devuelve el cliente si existe', async () => {
prismaMock.clienteMaestro.findUnique.mockResolvedValue({
id: '1', numCliente: 'C001', nombre: 'Juan', importadoEn: new Date(),
})
const result = await obtener('C001')
expect(result).not.toBeNull()
expect(result!.numCliente).toBe('C001')
})
it('devuelve null si no existe', async () => {
prismaMock.clienteMaestro.findUnique.mockResolvedValue(null)
expect(await obtener('NOPE')).toBeNull()
})
})
// ── contar ───────────────────────────────────────────────────────────
describe('contar', () => {
it('devuelve el conteo total', async () => {
prismaMock.clienteMaestro.count.mockResolvedValue(42)
expect(await contar({})).toBe(42)
})
it('cuenta con filtros', async () => {
prismaMock.clienteMaestro.count.mockResolvedValue(5)
expect(await contar({ desarrollo: 'SENDA' })).toBe(5)
expect(prismaMock.clienteMaestro.count).toHaveBeenCalledWith(
expect.objectContaining({ where: { desarrollo: 'SENDA' } }),
)
})
})
// ── exportar ─────────────────────────────────────────────────────────
describe('exportar', () => {
it('devuelve array de clientes', async () => {
prismaMock.clienteMaestro.findMany.mockResolvedValue([
{ id: '1', numCliente: 'C001', nombre: 'Juan', importadoEn: new Date() },
{ id: '2', numCliente: 'C002', nombre: 'Ana', importadoEn: new Date() },
])
const result = await exportar({})
expect(result).toHaveLength(2)
expect(prismaMock.clienteMaestro.findMany).toHaveBeenCalledWith(
expect.objectContaining({ take: 50000 }),
)
})
})
// ── importarDesdeExcel ───────────────────────────────────────────────
describe('importarDesdeExcel', () => {
it('inserta clientes nuevos', async () => {
prismaMock.clienteMaestro.findUnique.mockResolvedValue(null)
prismaMock.clienteMaestro.create.mockResolvedValue({})
const result = await importarDesdeExcel([
{ numCliente: 'C001', nombre: 'Juan', apellidoPaterno: 'Perez' },
])
expect(result.insertados).toBe(1)
expect(result.actualizados).toBe(0)
expect(result.errores).toBe(0)
expect(prismaMock.clienteMaestro.create).toHaveBeenCalled()
})
it('actualiza clientes existentes', async () => {
prismaMock.clienteMaestro.findUnique.mockResolvedValue({ id: '1', numCliente: 'C001' })
prismaMock.clienteMaestro.update.mockResolvedValue({})
const result = await importarDesdeExcel([
{ numCliente: 'C001', nombre: 'Juan', apellidoPaterno: 'Perez' },
])
expect(result.insertados).toBe(0)
expect(result.actualizados).toBe(1)
expect(result.errores).toBe(0)
expect(prismaMock.clienteMaestro.update).toHaveBeenCalled()
})
it('salta registros sin numCliente o nombre', async () => {
const result = await importarDesdeExcel([
{ numCliente: '', nombre: 'Juan' },
{ numCliente: 'C001', nombre: '' },
])
expect(result.errores).toBe(2)
expect(prismaMock.clienteMaestro.create).not.toHaveBeenCalled()
})
it('maneja errores de DB como errores', async () => {
prismaMock.clienteMaestro.findUnique.mockResolvedValue(null)
prismaMock.clienteMaestro.create.mockRejectedValue(new Error('DB error'))
const result = await importarDesdeExcel([
{ numCliente: 'C001', nombre: 'Juan', apellidoPaterno: 'Perez' },
])
expect(result.errores).toBe(1)
expect(result.insertados).toBe(0)
})
})