import { describe, expect, it, vi } from 'vitest' import { cambiarEstadoLead, crearLead, descartarLead, ESTADOS_LEAD_PERMITIDOS, } from './leads.controller.js' import type { LeadMonday } from '../services/monday.service.js' /** Monday mockeado: registra escrituras y devuelve catálogos controlados */ function mondayMock(opts: { labelsOrigen?: string[]; duplicado?: LeadMonday | null } = {}) { return { cambios: [] as { boardId: string; itemId: string; columna: string; valor: unknown }[], motivos: [] as { nombre: string; categoria: string; descripcion?: string }[], leadsCreados: [] as Record[], async cambiarValorColumna(boardId: string, itemId: string, columna: string, valor: string | number | boolean) { this.cambios.push({ boardId, itemId, columna, valor }) }, async crearLead(datos: Record) { this.leadsCreados.push(datos) return 'lead-nuevo-1' }, async buscarLeadPorTelefono(_telefono: string) { return opts.duplicado ?? null }, async labelsDeColumna(_boardId: string, _titulo: string) { return opts.labelsOrigen ?? ['WhatsApp', 'Web', 'Recomendado', 'Facebook', 'Otro'] }, async obtenerNombreItem(_itemId: string) { return 'Lead Prueba' }, async crearMotivoNoCompra(datos: { nombre: string; categoria: string; descripcion?: string }) { this.motivos.push(datos) return 'motivo-item-1' }, } } const datosBase = { nombre: 'Lead Prueba', telefono: '4771234567' } describe('crearLead (POST /api/leads)', () => { it('crea el item en Monday con Estado NUEVO y devuelve su id', async () => { const monday = mondayMock() const r = await crearLead(monday, { ...datosBase, origen: 'Facebook', notas: 'Vio el volante' }) expect(r).toEqual({ id: 'lead-nuevo-1' }) expect(monday.leadsCreados).toEqual([ { nombre: 'Lead Prueba', telefono: '4771234567', email: undefined, origenLabel: 'Facebook', notas: 'Vio el volante' }, ]) }) it('duplicado por teléfono: no crea nada y devuelve el lead existente (endpoint → 409)', async () => { const existente: LeadMonday = { id: 'lead-viejo-9', nombre: 'Lead Prueba', createdAt: '', updatedAt: '', estado: 'NUEVO', origen: '', telefono: '4771234567', email: '', vendedorIds: [], } const monday = mondayMock({ duplicado: existente }) const r = await crearLead(monday, datosBase) expect(r).toEqual({ duplicado: existente }) expect(monday.leadsCreados).toEqual([]) }) it('rechaza un origen que no sea label real de la columna Origen', async () => { const monday = mondayMock() await expect(crearLead(monday, { ...datosBase, origen: 'Volanteo' })).rejects.toThrow(/Origen inválido/) expect(monday.leadsCreados).toEqual([]) }) }) describe('cambiarEstadoLead (POST /api/leads/:itemId/estado)', () => { it('la whitelist solo permite las transiciones del vendedor', () => { expect(ESTADOS_LEAD_PERMITIDOS).toEqual(['CONTACTADO', 'Intento de contacto', 'AGENDA CITA', 'CALIFICADO']) // DESCARTADO tiene su propio endpoint (lleva motivo); NUEVO no es acción expect(ESTADOS_LEAD_PERMITIDOS).not.toContain('DESCARTADO') expect(ESTADOS_LEAD_PERMITIDOS).not.toContain('NUEVO') }) it('cambia solo la columna Estado en el board Leads (444 en tests)', async () => { const monday = mondayMock() await cambiarEstadoLead(monday, 'lead-1', 'CONTACTADO') expect(monday.cambios).toEqual([{ boardId: '444', itemId: 'lead-1', columna: 'Estado', valor: 'CONTACTADO' }]) }) it('AGENDA CITA con fecha también setea la columna Fecha', async () => { const monday = mondayMock() await cambiarEstadoLead(monday, 'lead-1', 'AGENDA CITA', '2026-09-01') expect(monday.cambios).toEqual([ { boardId: '444', itemId: 'lead-1', columna: 'Estado', valor: 'AGENDA CITA' }, { boardId: '444', itemId: 'lead-1', columna: 'Fecha', valor: '2026-09-01' }, ]) }) it('AGENDA CITA sin fecha no toca la columna Fecha', async () => { const monday = mondayMock() await cambiarEstadoLead(monday, 'lead-1', 'AGENDA CITA') expect(monday.cambios).toHaveLength(1) }) }) describe('descartarLead (POST /api/leads/:itemId/descartar)', () => { it('Estado DESCARTADO + Notas + registro en Motivos No Compra', async () => { const monday = mondayMock() const motivoItemId = await descartarLead(monday, 'lead-1', 'Precio', 'Se le hizo caro') expect(motivoItemId).toBe('motivo-item-1') expect(monday.cambios).toEqual([ { boardId: '444', itemId: 'lead-1', columna: 'Estado', valor: 'DESCARTADO' }, { boardId: '444', itemId: 'lead-1', columna: 'Notas', valor: 'Se le hizo caro' }, ]) expect(monday.motivos).toEqual([ { nombre: 'Precio — Lead Prueba', categoria: 'Precio', descripcion: 'Se le hizo caro' }, ]) }) it('sin comentario no escribe Notas', async () => { const monday = mondayMock() await descartarLead(monday, 'lead-1', 'Timing') expect(monday.cambios).toEqual([{ boardId: '444', itemId: 'lead-1', columna: 'Estado', valor: 'DESCARTADO' }]) expect(monday.motivos[0]).toEqual({ nombre: 'Timing — Lead Prueba', categoria: 'Timing' }) }) it('propaga el error si Monday falla (el error handler responde 5xx, no truena el server)', async () => { const monday = mondayMock() vi.spyOn(monday, 'cambiarValorColumna').mockRejectedValueOnce(new Error('Monday caído')) await expect(descartarLead(monday, 'lead-1', 'Cliente')).rejects.toThrow('Monday caído') expect(monday.motivos).toEqual([]) }) })