import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest' import type { Request, Response } from 'express' import { cambiarEstadoApartado, cambiarEstadoApartadoEndpoint, FASES_APARTADO_PERMITIDAS, subirDocumentosApartado, TIPOS_DOCUMENTO_APARTADO, } from './apartados.controller.js' import { saludMonday } from '../services/salud-monday.service.js' // ---------- Prisma mockeado (los tests no tocan la BD) ---------- const prismaMock = vi.hoisted(() => ({ leadCaptura: { create: vi.fn(), findFirst: vi.fn(), findUnique: vi.fn(), update: vi.fn(), }, cliente: { create: vi.fn(), findFirst: vi.fn(), }, documento: { create: vi.fn(), }, syncPendiente: { create: vi.fn(), }, auditoriaLog: { create: vi.fn().mockResolvedValue({}), }, $transaction: vi.fn(), })) vi.mock('../services/prisma.js', () => ({ prisma: prismaMock })) /** Monday mockeado: registra cambios de columna y movimientos de grupo */ function mondayMock(grupoActual = 'topics') { return { cambios: [] as { boardId: string; itemId: string; columna: string; valor: unknown }[], movimientos: [] as { itemId: string; grupo: string }[], async cambiarValorColumna(boardId: string, itemId: string, columna: string, valor: string | number | boolean) { this.cambios.push({ boardId, itemId, columna, valor }) }, async columnaIdPorTitulo(_boardId: string, titulo: string) { return titulo === 'Estado' ? 'status' : null }, async getItemConGrupo(itemId: string) { return { id: itemId, name: 'Cliente Demo', groupId: grupoActual, column_values: [ { id: 'status', text: (this.cambios.find((c) => c.columna === 'Estado')?.valor as string) ?? '', value: '{}' }, ], } }, async moverItemAGrupo(itemId: string, grupo: string) { this.movimientos.push({ itemId, grupo }) }, } } describe('cambiarEstadoApartado (POST /api/apartados/:id/estado)', () => { it('cambia la fase (board 666 = MONDAY_BOARD_APARTADO de test)', async () => { const monday = mondayMock() await cambiarEstadoApartado(monday, 'apt-1', 'Alta Enkontrol') expect(monday.cambios).toEqual([ { boardId: '666', itemId: 'apt-1', columna: 'Estado', valor: 'Alta Enkontrol' }, ]) // 'Alta Enkontrol' mapea al grupo topics: ya estaba ahí → no se mueve expect(monday.movimientos).toEqual([]) }) it('Firma Cliente (Venta) mueve el item a "Clientes Activos"', async () => { const monday = mondayMock() await cambiarEstadoApartado(monday, 'apt-2', 'Firma Cliente (Venta)') expect(monday.movimientos).toEqual([{ itemId: 'apt-2', grupo: 'group_mm6sx3rn' }]) }) it('Cancelacion Apartado mueve el item a "Historico"', async () => { const monday = mondayMock() await cambiarEstadoApartado(monday, 'apt-3', 'Cancelacion Apartado') expect(monday.movimientos).toEqual([{ itemId: 'apt-3', grupo: 'group_mm6s2yyx' }]) }) it('las fases permitidas son los labels reales del board Apartado', () => { expect(FASES_APARTADO_PERMITIDAS).toEqual([ 'Pago Apartado', 'Alta Enkontrol', 'Entrego Documentacion', 'Cita Clientes', 'Contrato Enkontrol', 'Firma Cliente (Venta)', 'Cancelacion Apartado', ]) }) it('los tipos de documento de la precarga son capturas obligatorias + generales', () => { expect(TIPOS_DOCUMENTO_APARTADO).toEqual(['CAPTURA_COTIZADOR', 'CAPTURA_PAGO_APARTADO', 'OTRO']) }) }) // ---------- Modo local (saludMonday caído / ids loc_*) ---------- describe('ruteo modo local (apartados)', () => { function resFake() { const json = vi.fn() const status = vi.fn().mockReturnValue({ json }) return { res: { json, status } as unknown as Response, status, json } } beforeEach(() => { vi.clearAllMocks() prismaMock.leadCaptura.findUnique.mockResolvedValue(null) prismaMock.leadCaptura.findFirst.mockResolvedValue(null) prismaMock.leadCaptura.create.mockImplementation(async ({ data }) => ({ ...data })) prismaMock.leadCaptura.update.mockImplementation(async ({ data }) => ({ ...data })) prismaMock.cliente.findFirst.mockResolvedValue(null) prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-local', ...data })) prismaMock.documento.create.mockImplementation(async ({ data }) => ({ id: 'doc-1', ...data })) prismaMock.syncPendiente.create.mockResolvedValue({}) prismaMock.$transaction.mockImplementation(async (ops: Promise[]) => Promise.all(ops)) }) afterEach(() => { vi.restoreAllMocks() }) it('cambiarEstadoApartadoEndpoint con id loc_*: fase local + CAMBIO_ETAPA encolado', async () => { vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(true) // el id loc_* manda aunque Monday viva prismaMock.leadCaptura.findUnique.mockResolvedValue({ leadId: 'loc_apt', board: 'APARTADO' }) const { res, json } = resFake() await cambiarEstadoApartadoEndpoint( { params: { itemId: 'loc_apt' }, body: { estado: 'Alta Enkontrol' } } as unknown as Request, res, ) expect(prismaMock.leadCaptura.update).toHaveBeenCalledWith({ where: { leadId: 'loc_apt' }, data: { etapa: 'Alta Enkontrol', pendienteSync: true }, }) expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({ data: expect.objectContaining({ tipo: 'CAMBIO_ETAPA', referenciaLocal: 'loc_apt' }), }) expect(json).toHaveBeenCalledWith({ itemId: 'loc_apt', estado: 'Alta Enkontrol' }) }) it('subirDocumentosApartado con Monday caído: nombre desde LeadCaptura local y checkbox encolado (CHECKBOX_DOC)', async () => { vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false) // El apartado local nació del "Aparto" de la oportunidad: su fila tiene el nombre prismaMock.leadCaptura.findFirst.mockResolvedValue({ leadId: 'loc_apt', nombreLead: 'Luis Demo' }) const archivo = { originalname: 'curp.pdf', path: '/tmp/uploads/apartados/loc_apt/CURP_1.pdf', mimetype: 'application/pdf', size: 1234, } as Express.Multer.File const { res, status } = resFake() await subirDocumentosApartado( { params: { itemId: 'loc_apt' }, body: { tipo: 'CURP' }, files: [archivo], usuarioToken: { sub: 'u-1' } } as unknown as Request, res, ) // Cliente mínimo creado con el nombre de la fila local (no truena por obtenerNombreItem) expect(prismaMock.cliente.create).toHaveBeenCalledWith({ data: expect.objectContaining({ nombreCompleto: 'Luis Demo', mondayApartadoId: 'loc_apt' }), }) expect(prismaMock.documento.create).toHaveBeenCalledWith({ data: expect.objectContaining({ clienteId: 'cli-local', tipo: 'CURP' }), }) // El checkbox no se intenta en Monday: queda encolado para el worker expect(prismaMock.syncPendiente.create).toHaveBeenCalledWith({ data: expect.objectContaining({ tipo: 'CHECKBOX_DOC', payloadJson: JSON.stringify({ checkbox: 'CURP', itemId: 'loc_apt' }), referenciaLocal: 'loc_apt', }), }) expect(status).toHaveBeenCalledWith(201) }) })