feat: vínculo con usuario Monday en landing + alta prellenada desde leads

API:
- GET /api/monday/usuarios: usuarios activos no-guest del workspace
- PATCH /api/usuarios/me/monday: vincula/desvincula el mondayUserId del
  usuario autenticado (valida contra el workspace)
- GET /api/leads/buscar?q=: búsqueda de leads por nombre/teléfono/email
  (sin acentos, orden updatedAt desc, límite 15)
- aPublico/me ahora incluyen mondayUserId

Landing (/):
- Nueva sección 'Configuración de cuenta': login inline, selector del
  usuario de Monday, guardar/desvincular, cerrar sesión

Alta de clientes (/client):
- Búsqueda de leads de Monday con debounce en la pantalla de entrada;
  al elegir uno prellena nombre/email/celular y guarda leadMondayId
- El picker (borradores + alta nueva + búsqueda de lead) es ahora
  siempre la pantalla de entrada

Tests: api 294, landing 8, alta-clientes 96 — todos verdes
This commit is contained in:
2026-08-28 18:36:08 +00:00
parent d99d90d344
commit 1692dde887
28 changed files with 2501 additions and 41 deletions

View File

@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Request, Response } from 'express'
import type { Usuario } from '@prisma/client'
import {
actualizarVinculoMondayEndpoint,
listarUsuariosMondayEndpoint,
} from './usuarios.controller.js'
import { AppError } from '../utils/errors.js'
// ---------- Monday y Prisma mockeados (los tests no tocan red ni BD) ----------
const mondayMock = vi.hoisted(() => ({ getUsuariosWorkspace: vi.fn() }))
const prismaMock = vi.hoisted(() => ({ usuario: { update: vi.fn() } }))
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
const USUARIOS_WORKSPACE = [
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
]
function usuarioFake(overrides: Partial<Usuario> = {}): Usuario {
return {
id: 'u-1',
username: 'erick israel pena ramos',
nombre: 'ERICK ISRAEL PEÑA RAMOS',
passwordHash: 'hash',
rol: 'vendedor',
asignacion: 'MONTEBELLO',
empleadoId: 13676,
mondayUserId: null,
activo: true,
createdAt: new Date(),
updatedAt: new Date(),
...overrides,
}
}
function reqFake(overrides: Partial<Request> = {}): Request {
return {
body: {},
usuarioToken: { sub: 'u-1', username: 'erick israel pena ramos', rol: 'vendedor' },
...overrides,
} as Request
}
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()
mondayMock.getUsuariosWorkspace.mockResolvedValue(USUARIOS_WORKSPACE)
})
describe('listarUsuariosMondayEndpoint (GET /api/monday/usuarios)', () => {
it('devuelve los usuarios del workspace con id numérico', async () => {
const { res, json } = resFake()
await listarUsuariosMondayEndpoint(reqFake(), res)
expect(json).toHaveBeenCalledWith([
{ id: 13676, nombre: 'Erick Peña', email: 'erick@frasa.mx' },
{ id: 20400, nombre: 'Cristina Barrera', email: 'cristina@frasa.mx' },
])
})
it('propaga el error si Monday falla (el error handler responde 5xx)', async () => {
mondayMock.getUsuariosWorkspace.mockRejectedValue(new Error('Monday caído'))
const { res } = resFake()
await expect(listarUsuariosMondayEndpoint(reqFake(), res)).rejects.toThrow('Monday caído')
})
})
describe('actualizarVinculoMondayEndpoint (PATCH /api/usuarios/me/monday)', () => {
it('con id válido guarda el vínculo y devuelve el perfil actualizado', async () => {
prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: 13676 }))
const { res, json } = resFake()
await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: 13676 } }), res)
expect(prismaMock.usuario.update).toHaveBeenCalledWith({
where: { id: 'u-1' },
data: { mondayUserId: 13676 },
})
expect(json).toHaveBeenCalledWith({
nombre: 'ERICK ISRAEL PEÑA RAMOS',
rol: 'vendedor',
asignacion: 'MONTEBELLO',
mondayUserId: 13676,
})
})
it('con id inexistente en el workspace responde 400 y no toca la BD', async () => {
const { res } = resFake()
const error = await actualizarVinculoMondayEndpoint(
reqFake({ body: { mondayUserId: 99999 } }),
res,
).catch((e: unknown) => e)
expect(error).toBeInstanceOf(AppError)
expect(error).toMatchObject({ status: 400 })
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
})
it('con null desvincula sin consultar Monday', async () => {
prismaMock.usuario.update.mockResolvedValue(usuarioFake({ mondayUserId: null }))
const { res, json } = resFake()
await actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: null } }), res)
expect(mondayMock.getUsuariosWorkspace).not.toHaveBeenCalled()
expect(prismaMock.usuario.update).toHaveBeenCalledWith({
where: { id: 'u-1' },
data: { mondayUserId: null },
})
expect(json).toHaveBeenCalledWith(expect.objectContaining({ mondayUserId: null }))
})
it('sin token verificado: 400', async () => {
const { res } = resFake()
await expect(
actualizarVinculoMondayEndpoint(reqFake({ usuarioToken: undefined }), res),
).rejects.toThrow('Falta el token verificado')
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
})
it('body inválido (string): ZodError (el error handler lo traduce a 400)', async () => {
const { res } = resFake()
await expect(
actualizarVinculoMondayEndpoint(reqFake({ body: { mondayUserId: '13676' } }), res),
).rejects.toThrow()
expect(prismaMock.usuario.update).not.toHaveBeenCalled()
})
})