- App Mi día: KPIs, mis actividades, mis leads con acciones rápidas (contactado/intento/agenda cita/descartar con motivo), captura rápida de lead (409 en duplicado por teléfono), vista equipo para coordinadores - API: GET /api/mi-dia/resumen, POST /api/leads, /leads/:id/estado, /leads/:id/descartar; mapeo Usuario.mondayUserId (seed listo para cuando inviten a los vendedores a Monday) - Landing: tarjeta Mi día; tests api 188, mi-dia 8, landing 4
134 lines
5.5 KiB
TypeScript
134 lines
5.5 KiB
TypeScript
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<string, unknown>[],
|
|
async cambiarValorColumna(boardId: string, itemId: string, columna: string, valor: string | number | boolean) {
|
|
this.cambios.push({ boardId, itemId, columna, valor })
|
|
},
|
|
async crearLead(datos: Record<string, unknown>) {
|
|
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([])
|
|
})
|
|
})
|