1. apartadosPorVencer: filtrado por esMio — cada vendedor solo ve sus propios apartados por vencer, no los de todos. 2. misLeads: excluye items que ya estan en misOportunidades — evita que el mismo item aparezca en ambas pestanas (leads + oportunidades). 3. 630 tests verdes.
610 lines
26 KiB
TypeScript
610 lines
26 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
detalleEventoUltimoCambio,
|
|
esEstadoFinalApartado,
|
|
esEstadoTerminalLead,
|
|
esEtapaTerminal,
|
|
resumenMiDia,
|
|
type UsuarioMiDia,
|
|
} from './mi-dia.service.js'
|
|
import type {
|
|
ActividadMonday,
|
|
ApartadoMonday,
|
|
LeadMonday,
|
|
OportunidadMonday,
|
|
UltimoCambioMonday,
|
|
} from './monday.service.js'
|
|
|
|
const MI_ID = 106736727
|
|
const OTRO_ID = 111111111
|
|
|
|
const AHORA = new Date('2026-08-28T12:00:00') // viernes
|
|
const HOY = '2026-08-28'
|
|
const MANANA = '2026-08-29'
|
|
|
|
function lead(parcial: Partial<LeadMonday> & { id: string }): LeadMonday {
|
|
return {
|
|
nombre: `Lead ${parcial.id}`,
|
|
createdAt: '2026-08-25T10:00:00Z',
|
|
updatedAt: '2026-08-27T10:00:00Z',
|
|
estado: 'Nuevo',
|
|
origen: 'Facebook',
|
|
telefono: '',
|
|
email: '',
|
|
vendedorIds: [MI_ID],
|
|
...parcial,
|
|
}
|
|
}
|
|
|
|
function oportunidad(parcial: Partial<OportunidadMonday> & { id: string }): OportunidadMonday {
|
|
return {
|
|
nombre: `Lead ${parcial.id}`,
|
|
createdAt: '2026-08-25T10:00:00Z',
|
|
updatedAt: '2026-08-27T10:00:00Z',
|
|
etapa: 'Afluencia',
|
|
origen: 'Facebook',
|
|
unidadCodigo: '',
|
|
precioFinal: 0,
|
|
asesorIds: [MI_ID],
|
|
telefono: '',
|
|
email: '',
|
|
...parcial,
|
|
}
|
|
}
|
|
|
|
function actividad(parcial: Partial<ActividadMonday> & { id: string }): ActividadMonday {
|
|
return {
|
|
nombre: `Actividad ${parcial.id}`,
|
|
tipo: 'Llamada',
|
|
fecha: HOY,
|
|
estado: 'Abierto',
|
|
responsableIds: [MI_ID],
|
|
...parcial,
|
|
}
|
|
}
|
|
|
|
function apartado(parcial: Partial<ApartadoMonday> & { id: string }): ApartadoMonday {
|
|
return {
|
|
nombre: `Cliente ${parcial.id}`,
|
|
unidadCodigo: 'U-101',
|
|
fechaLimite: '2026-09-02', // dentro de 7 días
|
|
estado: 'Pago Apartado',
|
|
vendedorIds: [MI_ID],
|
|
...parcial,
|
|
}
|
|
}
|
|
|
|
function mondayMock(datos: {
|
|
leads?: LeadMonday[]
|
|
oportunidades?: OportunidadMonday[]
|
|
actividades?: ActividadMonday[]
|
|
apartados?: ApartadoMonday[]
|
|
/** boardId → (itemId → último cambio); boards de test: Leads '444', Oportunidades '555' */
|
|
ultimosCambios?: Record<string, Map<string, UltimoCambioMonday>>
|
|
}) {
|
|
return {
|
|
async getLeads() {
|
|
return datos.leads ?? []
|
|
},
|
|
async getOportunidades() {
|
|
return datos.oportunidades ?? []
|
|
},
|
|
async getActividades() {
|
|
return datos.actividades ?? []
|
|
},
|
|
async getApartados() {
|
|
return datos.apartados ?? []
|
|
},
|
|
async getUltimosCambios(boardId: string, _itemIds: string[]) {
|
|
return datos.ultimosCambios?.[boardId] ?? new Map<string, UltimoCambioMonday>()
|
|
},
|
|
}
|
|
}
|
|
|
|
const vendedor: UsuarioMiDia = { nombre: 'Martin Zavala', rol: 'vendedor', mondayUserId: MI_ID }
|
|
const sinMapeo: UsuarioMiDia = { nombre: 'Erick Peña', rol: 'vendedor', mondayUserId: null }
|
|
const coordinador: UsuarioMiDia = { nombre: 'Daniel Alvarado', rol: 'coordinador', mondayUserId: null }
|
|
|
|
describe('resumenMiDia (GET /api/mi-dia/resumen)', () => {
|
|
it('misLeads: merge de Leads (Vendedor=mío) y Oportunidades (Asesor=mío), sin terminales, con fuente, por updatedAt desc', async () => {
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1', estado: 'Nuevo', updatedAt: '2026-08-26T10:00:00Z' }),
|
|
lead({ id: 'l2', estado: 'Contactado', telefono: '4771234567', updatedAt: '2026-08-28T08:00:00Z' }),
|
|
lead({ id: 'l3', estado: 'Descartado' }), // terminal Leads: fuera
|
|
lead({ id: 'l4', estado: 'Calificado' }), // terminal Leads: fuera
|
|
lead({ id: 'l5', estado: 'Nuevo', vendedorIds: [OTRO_ID] }), // de otro: fuera
|
|
],
|
|
oportunidades: [
|
|
oportunidad({ id: 'o1', etapa: 'Afluencia', updatedAt: '2026-08-27T10:00:00Z' }),
|
|
oportunidad({ id: 'o2', etapa: 'Calificado' }), // NO es terminal en Oportunidades
|
|
oportunidad({ id: 'o3', etapa: 'Perdido' }),
|
|
oportunidad({ id: 'o4', etapa: 'Abandono' }),
|
|
oportunidad({ id: 'o5', etapa: 'Ganado' }),
|
|
oportunidad({ id: 'o6', etapa: 'Afluencia', asesorIds: [OTRO_ID] }),
|
|
],
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.sinMapeoMonday).toBe(false)
|
|
expect(r.misLeads.map((l) => l.id)).toEqual(['l2', 'o1', 'o2', 'l1']) // updatedAt desc mezclando tableros
|
|
expect(r.misLeads.map((l) => l.fuente)).toEqual(['lead', 'oportunidad', 'oportunidad', 'lead'])
|
|
expect(r.misLeads[0]?.telefono).toBe('4771234567')
|
|
})
|
|
|
|
it('ultimoCambio: último evento por item, agrupando ids por fuente y resolviendo el nombre', async () => {
|
|
const monday = mondayMock({
|
|
leads: [lead({ id: 'l1', updatedAt: '2026-08-28T08:00:00Z' })],
|
|
oportunidades: [oportunidad({ id: 'o1', updatedAt: '2026-08-27T08:00:00Z' })],
|
|
ultimosCambios: {
|
|
'444': new Map([['l1', { fechaISO: '2026-08-28T10:00:00.000Z', usuarioId: MI_ID, evento: 'update_status' }]]),
|
|
'555': new Map([['o1', { fechaISO: '2026-08-28T09:00:00.000Z', usuarioId: OTRO_ID, evento: 'create_pulse' }]]),
|
|
},
|
|
})
|
|
const nombres = new Map([[MI_ID, 'Martin Zavala']]) // OTRO_ID no está mapeado
|
|
const r = await resumenMiDia(monday, vendedor, nombres, [], [], AHORA)
|
|
expect(r.misLeads.find((l) => l.id === 'l1')?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-08-28T10:00:00.000Z',
|
|
usuario: 'Martin Zavala',
|
|
detalle: 'Actualización',
|
|
})
|
|
// usuarioId sin nombre en el mapa → fallback null
|
|
expect(r.misLeads.find((l) => l.id === 'o1')?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-08-28T09:00:00.000Z',
|
|
usuario: null,
|
|
detalle: 'Alta',
|
|
})
|
|
})
|
|
|
|
it('ultimoCambio: null cuando el item no tiene eventos en el activity log', async () => {
|
|
const monday = mondayMock({
|
|
leads: [lead({ id: 'l1' })],
|
|
ultimosCambios: { '444': new Map() }, // sin eventos
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misLeads[0]?.ultimoCambio).toBeNull()
|
|
})
|
|
|
|
it('ultimoCambio: usuarioId null (evento de automatización) → usuario null', async () => {
|
|
const monday = mondayMock({
|
|
leads: [lead({ id: 'l1' })],
|
|
ultimosCambios: {
|
|
'444': new Map([['l1', { fechaISO: '2026-08-28T10:00:00.000Z', usuarioId: null, evento: 'move_to_group' }]]),
|
|
},
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misLeads[0]?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-08-28T10:00:00.000Z',
|
|
usuario: null,
|
|
detalle: 'Cambio de grupo',
|
|
})
|
|
})
|
|
|
|
it('detalleEventoUltimoCambio: humaniza los eventos conocidos y pasa el crudo si no', () => {
|
|
expect(detalleEventoUltimoCambio('create_pulse')).toBe('Alta')
|
|
expect(detalleEventoUltimoCambio('create_update')).toBe('Nota agregada')
|
|
expect(detalleEventoUltimoCambio('update_column_value')).toBe('Actualización')
|
|
expect(detalleEventoUltimoCambio('update_status')).toBe('Actualización')
|
|
expect(detalleEventoUltimoCambio('move_to_group')).toBe('Cambio de grupo')
|
|
expect(detalleEventoUltimoCambio('update_name')).toBe('update_name')
|
|
})
|
|
|
|
it('misOportunidades: conteo de MIS oportunidades por etapa (incluye terminales)', async () => {
|
|
const monday = mondayMock({
|
|
oportunidades: [
|
|
oportunidad({ id: '1', etapa: 'Afluencia' }),
|
|
oportunidad({ id: '2', etapa: 'Afluencia' }),
|
|
oportunidad({ id: '3', etapa: 'Ganado' }),
|
|
oportunidad({ id: '4', etapa: 'Afluencia', asesorIds: [OTRO_ID] }), // de otro: no cuenta
|
|
],
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misOportunidades.porEtapa).toEqual({ Afluencia: 2, Ganado: 1 })
|
|
})
|
|
|
|
it('misOportunidades.items: lista de trabajo sin terminales, mías, por updatedAt desc, con perfil y último cambio', async () => {
|
|
const monday = mondayMock({
|
|
oportunidades: [
|
|
oportunidad({ id: 'o1', etapa: 'Afluencia', updatedAt: '2026-08-27T10:00:00Z', desarrollo: 'SENDA', modeloInteres: 'SOHO' }),
|
|
oportunidad({ id: 'o2', etapa: 'Cotizador', updatedAt: '2026-08-28T08:00:00Z', telefono: '4771234567', email: 'a@b.c', unidadCodigo: 'U-9' }),
|
|
oportunidad({ id: 'o3', etapa: 'Ganado' }), // terminal: fuera de la lista (sí cuenta en porEtapa)
|
|
oportunidad({ id: 'o4', etapa: 'Abandono' }), // terminal: fuera
|
|
oportunidad({ id: 'o5', etapa: 'Afluencia', asesorIds: [OTRO_ID] }), // de otro: fuera
|
|
oportunidad({ id: 'o6', etapa: 'Aparto' }), // ya es apartado: sale de la lista (sigue en Mis apartados)
|
|
],
|
|
ultimosCambios: {
|
|
'555': new Map([['o2', { fechaISO: '2026-08-28T09:00:00.000Z', usuarioId: MI_ID, evento: 'update_column_value' }]]),
|
|
},
|
|
})
|
|
const nombres = new Map([[MI_ID, 'Martin Zavala']])
|
|
const r = await resumenMiDia(monday, vendedor, nombres, [], [], AHORA)
|
|
expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['o2', 'o1'])
|
|
const o2 = r.misOportunidades.items[0]
|
|
expect(o2?.etapa).toBe('Cotizador')
|
|
expect(o2?.telefono).toBe('4771234567')
|
|
expect(o2?.email).toBe('a@b.c')
|
|
expect(o2?.unidadCodigo).toBe('U-9')
|
|
expect(o2?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-08-28T09:00:00.000Z',
|
|
usuario: 'Martin Zavala',
|
|
detalle: 'Actualización',
|
|
})
|
|
const o1 = r.misOportunidades.items[1]
|
|
expect(o1?.desarrollo).toBe('SENDA')
|
|
expect(o1?.modeloInteres).toBe('SOHO')
|
|
expect(o1?.ultimoCambio).toBeNull()
|
|
})
|
|
|
|
it('misApartados: Vendedor = yo, sin estados finales, por fecha límite asc (sin fecha al final), con último cambio', async () => {
|
|
const monday = mondayMock({
|
|
apartados: [
|
|
apartado({ id: 'p1', fechaLimite: '2026-09-05' }),
|
|
apartado({ id: 'p2', fechaLimite: '2026-09-01', estado: 'Cita Clientes' }),
|
|
apartado({ id: 'p3', fechaLimite: '', estado: 'Alta Enkontrol' }),
|
|
apartado({ id: 'p4', estado: 'Firma Cliente (Venta)' }), // final: fuera
|
|
apartado({ id: 'p5', estado: 'Cancelacion Apartado' }), // final: fuera
|
|
apartado({ id: 'p6', vendedorIds: [OTRO_ID] }), // de otro: fuera
|
|
],
|
|
ultimosCambios: {
|
|
'666': new Map([['p1', { fechaISO: '2026-08-28T09:00:00.000Z', usuarioId: OTRO_ID, evento: 'create_update' }]]),
|
|
},
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misApartados.map((a) => a.id)).toEqual(['p2', 'p1', 'p3'])
|
|
expect(r.misApartados[0]?.estado).toBe('Cita Clientes')
|
|
expect(r.misApartados[1]?.ultimoCambio).toEqual({
|
|
fechaISO: '2026-08-28T09:00:00.000Z',
|
|
usuario: null, // OTRO_ID no está en el mapa de nombres
|
|
detalle: 'Nota agregada',
|
|
})
|
|
})
|
|
|
|
it('esEstadoFinalApartado: venta y cancelación cierran el pipeline', () => {
|
|
expect(esEstadoFinalApartado('Firma Cliente (Venta)')).toBe(true)
|
|
expect(esEstadoFinalApartado('Cancelacion Apartado')).toBe(true)
|
|
expect(esEstadoFinalApartado('Escriturado')).toBe(true)
|
|
expect(esEstadoFinalApartado('Pago Apartado')).toBe(false)
|
|
expect(esEstadoFinalApartado('Contrato Enkontrol')).toBe(false)
|
|
})
|
|
|
|
it('actividades de hoy: solo abiertas con fecha <= mañana y mías', async () => {
|
|
const monday = mondayMock({
|
|
actividades: [
|
|
actividad({ id: 'a1', fecha: HOY }),
|
|
actividad({ id: 'a2', fecha: MANANA, tipo: 'Reunion' }),
|
|
actividad({ id: 'a3', fecha: '2026-08-20' }), // vencida: también entra
|
|
actividad({ id: 'a4', fecha: '2026-09-05' }), // futura lejana: no
|
|
actividad({ id: 'a5', fecha: HOY, estado: 'Listo' }), // cerrada: no
|
|
actividad({ id: 'a6', fecha: HOY, responsableIds: [OTRO_ID] }), // de otro: no
|
|
actividad({ id: 'a7', fecha: '' }), // sin fecha: no
|
|
],
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
// Ordenadas por fecha: vencida primero
|
|
expect(r.misActividadesHoy.map((a) => a.id)).toEqual(['a3', 'a1', 'a2'])
|
|
expect(r.conteos.actividadesAbiertas).toBe(5) // abiertas mías: a1,a2,a3,a4,a7
|
|
})
|
|
|
|
it('apartados por vencer: próximos 7 días, excluye estados finales', async () => {
|
|
const monday = mondayMock({
|
|
apartados: [
|
|
apartado({ id: 'p1', fechaLimite: HOY }), // vence hoy
|
|
apartado({ id: 'p2', fechaLimite: '2026-09-04' }), // justo en 7 días
|
|
apartado({ id: 'p3', fechaLimite: '2026-09-05' }), // fuera de rango
|
|
apartado({ id: 'p4', fechaLimite: '2026-08-27' }), // ya venció
|
|
apartado({ id: 'p5', fechaLimite: '2026-09-01', estado: 'Escriturado' }),
|
|
apartado({ id: 'p6', fechaLimite: '2026-09-01', estado: 'Cancelacion' }),
|
|
],
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.apartadosPorVencer.map((a) => a.id)).toEqual(['p1', 'p2'])
|
|
expect(r.apartadosPorVencer[0]?.diasRestantes).toBe(0)
|
|
expect(r.apartadosPorVencer[1]?.diasRestantes).toBe(7)
|
|
})
|
|
|
|
it('sin mapeo Monday: listas personales vacías y sinMapeoMonday: true', async () => {
|
|
const monday = mondayMock({
|
|
oportunidades: [oportunidad({ id: '1' })],
|
|
actividades: [actividad({ id: 'a1' })],
|
|
apartados: [apartado({ id: 'p1' })],
|
|
})
|
|
const r = await resumenMiDia(monday, sinMapeo, new Map(), [], [], AHORA)
|
|
expect(r.sinMapeoMonday).toBe(true)
|
|
expect(r.misLeads).toEqual([])
|
|
expect(r.misOportunidades).toEqual({ porEtapa: {}, items: [] })
|
|
expect(r.misApartados).toEqual([])
|
|
expect(r.misActividadesHoy).toEqual([])
|
|
expect(r.conteos).toEqual({ leadsNuevosSemana: 0, actividadesAbiertas: 0 })
|
|
// Los apartados por vencer son del pipeline completo: sí se muestran
|
|
expect(r.apartadosPorVencer).toHaveLength(0) // solo apartados propios
|
|
})
|
|
|
|
it('conteos: leads nuevos de los últimos 7 días suman AMBOS tableros (incluye terminales recientes)', async () => {
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1', createdAt: '2026-08-28T08:00:00' }), // hoy
|
|
lead({ id: 'l2', createdAt: '2026-08-10T08:00:00' }), // viejo
|
|
],
|
|
oportunidades: [
|
|
oportunidad({ id: '1', createdAt: '2026-08-28T08:00:00' }), // hoy
|
|
oportunidad({ id: '2', createdAt: '2026-08-22T08:00:00' }), // hace 6 días
|
|
oportunidad({ id: '3', createdAt: '2026-08-10T08:00:00' }), // viejo
|
|
oportunidad({ id: '4', createdAt: '2026-08-27T08:00:00', asesorIds: [OTRO_ID] }), // de otro
|
|
],
|
|
})
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], [], AHORA)
|
|
expect(r.conteos.leadsNuevosSemana).toBe(3) // 1 lead + 2 oportunidades
|
|
})
|
|
|
|
it('vendedor no ve sección equipo; coordinador sí (conteos por persona de AMBOS tableros y sin asignar)', async () => {
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1', vendedorIds: [] }), // sin asignar, Nuevo
|
|
lead({ id: 'l2', vendedorIds: [], estado: 'Contactado' }), // sin asignar, no Nuevo
|
|
lead({ id: 'l3', vendedorIds: [OTRO_ID] }),
|
|
],
|
|
oportunidades: [
|
|
oportunidad({ id: '1', asesorIds: [] }), // sin asignar, Afluencia
|
|
oportunidad({ id: '2', asesorIds: [], etapa: 'Contactado' }), // sin asignar, no Afluencia
|
|
oportunidad({ id: '3', asesorIds: [MI_ID] }),
|
|
oportunidad({ id: '4', asesorIds: [MI_ID], etapa: 'Perdido' }), // terminal: no cuenta
|
|
oportunidad({ id: '5', asesorIds: [OTRO_ID] }),
|
|
oportunidad({ id: '6', asesorIds: [OTRO_ID], createdAt: '2026-08-10T08:00:00' }), // viejo: no es nuevo de la semana
|
|
],
|
|
})
|
|
const nombres = new Map([[MI_ID, 'Martin Zavala']])
|
|
|
|
const rVendedor = await resumenMiDia(monday, vendedor, nombres, [], [], AHORA)
|
|
expect(rVendedor.equipo).toBeUndefined()
|
|
|
|
const rCoord = await resumenMiDia(monday, coordinador, nombres, [], [], AHORA)
|
|
expect(rCoord.equipo?.leadsNuevosSinAsignar).toBe(2) // 1 lead Nuevo + 1 afluencia sin asignar
|
|
expect(rCoord.equipo?.porVendedor).toEqual([
|
|
{ mondayUserId: OTRO_ID, nombre: null, leadsAsignados: 3, leadsNuevosSemana: 2 }, // 1 lead + 2 ops (1 vieja)
|
|
{ mondayUserId: MI_ID, nombre: 'Martin Zavala', leadsAsignados: 1, leadsNuevosSemana: 1 },
|
|
])
|
|
})
|
|
|
|
it('corporativo y admin también ven equipo', async () => {
|
|
const monday = mondayMock({ oportunidades: [oportunidad({ id: '1' })] })
|
|
for (const rol of ['corporativo', 'admin']) {
|
|
const r = await resumenMiDia(monday, { nombre: 'X', rol, mondayUserId: null }, new Map(), [], [], AHORA)
|
|
expect(r.equipo).toBeDefined()
|
|
}
|
|
})
|
|
|
|
it('mis capturas: items en misLeads/misOportunidades no se duplican en misCapturas', async () => {
|
|
const monday = mondayMock({
|
|
oportunidades: [oportunidad({ id: '100', nombre: 'Ana López', etapa: 'Contactado', origen: 'WhatsApp', asesorIds: [OTRO_ID] })],
|
|
})
|
|
const capturas = [
|
|
{ leadId: '100', nombreLead: 'Ana López', telefono: '6641112233', createdAt: new Date('2026-08-27T09:00:00Z') },
|
|
]
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
|
|
// La captura ya aparece en misLeads (via esMio → idsCapturados), asi que no se duplica
|
|
expect(r.misCapturas).toEqual([])
|
|
})
|
|
|
|
it('mis capturas: captura rapida no se duplica si ya esta en misLeads', async () => {
|
|
const monday = mondayMock({
|
|
leads: [lead({ id: 'lead-9', nombre: 'Pepe Rápido', estado: 'Contactado', origen: 'Web', vendedorIds: [OTRO_ID] })],
|
|
})
|
|
const capturas = [
|
|
{ leadId: 'lead-9', nombreLead: 'Pepe Rápido', telefono: '4771112233', createdAt: new Date('2026-08-27T09:00:00Z') },
|
|
]
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
|
|
// La captura ya aparece en misLeads (via esMio → idsCapturados), asi que no se duplica
|
|
expect(r.misCapturas).toEqual([])
|
|
})
|
|
|
|
it('mis capturas: si el item ya no existe en ningún tablero, estado y origen van vacíos', async () => {
|
|
const monday = mondayMock({})
|
|
const capturas = [
|
|
{ leadId: 'borrado-1', nombreLead: 'Lead Borrado', telefono: null, createdAt: new Date('2026-08-26T09:00:00Z') },
|
|
]
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
|
|
expect(r.misCapturas).toEqual([
|
|
{ leadId: 'borrado-1', nombre: 'Lead Borrado', telefono: '', estado: '', origen: '', capturadoEn: '2026-08-26T09:00:00.000Z' },
|
|
])
|
|
})
|
|
|
|
it('mis capturas: ordenadas por fecha de captura descendente', async () => {
|
|
const monday = mondayMock({})
|
|
const capturas = [
|
|
{ leadId: '1', nombreLead: 'Antiguo', telefono: null, createdAt: new Date('2026-08-20T09:00:00Z') },
|
|
{ leadId: '2', nombreLead: 'Reciente', telefono: null, createdAt: new Date('2026-08-27T09:00:00Z') },
|
|
]
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA)
|
|
expect(r.misCapturas.map((c) => c.leadId)).toEqual(['2', '1'])
|
|
})
|
|
|
|
it('sin capturas: misCapturas va vacío', async () => {
|
|
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misCapturas).toEqual([])
|
|
})
|
|
|
|
it('mis clientes: mapeo con completado según proyecto (vacío = en captura)', async () => {
|
|
const clientes = [
|
|
{
|
|
id: 'c1',
|
|
nombreCompleto: 'Ana En Captura',
|
|
proyecto: '',
|
|
unidadCodigo: null,
|
|
mondayApartadoId: null,
|
|
createdAt: new Date('2026-08-26T09:00:00Z'),
|
|
},
|
|
{
|
|
id: 'c2',
|
|
nombreCompleto: 'Luis Completado',
|
|
proyecto: 'DIAGONAL',
|
|
unidadCodigo: 'U-101',
|
|
mondayApartadoId: null,
|
|
createdAt: new Date('2026-08-27T09:00:00Z'),
|
|
},
|
|
]
|
|
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], clientes, AHORA)
|
|
expect(r.misClientes).toEqual([
|
|
{
|
|
id: 'c2',
|
|
nombre: 'Luis Completado',
|
|
proyecto: 'DIAGONAL',
|
|
unidadCodigo: 'U-101',
|
|
completado: true,
|
|
capturadoEn: '2026-08-27T09:00:00.000Z',
|
|
estadoApartado: null,
|
|
},
|
|
{
|
|
id: 'c1',
|
|
nombre: 'Ana En Captura',
|
|
proyecto: '',
|
|
unidadCodigo: '',
|
|
completado: false,
|
|
capturadoEn: '2026-08-26T09:00:00.000Z',
|
|
estadoApartado: null,
|
|
},
|
|
])
|
|
})
|
|
|
|
it('mis clientes: estadoApartado cruza mondayApartadoId contra el tablero Apartado', async () => {
|
|
const clientes = [
|
|
{
|
|
id: 'c1',
|
|
nombreCompleto: 'Con Apartado',
|
|
proyecto: 'DIAGONAL',
|
|
unidadCodigo: 'U-101',
|
|
mondayApartadoId: 'ap-1',
|
|
createdAt: new Date('2026-08-27T09:00:00Z'),
|
|
},
|
|
{
|
|
id: 'c2',
|
|
nombreCompleto: 'Apartado Borrado',
|
|
proyecto: 'DIAGONAL',
|
|
unidadCodigo: 'U-102',
|
|
mondayApartadoId: 'ap-fantasma', // ya no existe en Monday
|
|
createdAt: new Date('2026-08-26T09:00:00Z'),
|
|
},
|
|
{
|
|
id: 'c3',
|
|
nombreCompleto: 'Sin Apartado',
|
|
proyecto: '',
|
|
unidadCodigo: null,
|
|
mondayApartadoId: null,
|
|
createdAt: new Date('2026-08-25T09:00:00Z'),
|
|
},
|
|
]
|
|
const monday = mondayMock({ apartados: [apartado({ id: 'ap-1', estado: 'Alta Enkontrol' })] })
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), [], clientes, AHORA)
|
|
expect(r.misClientes.map((c) => [c.id, c.estadoApartado])).toEqual([
|
|
['c1', 'Alta Enkontrol'], // estado real del item en Monday
|
|
['c2', null], // el apartado ya no existe
|
|
['c3', null], // sin apartado ligado
|
|
])
|
|
})
|
|
|
|
it('mis clientes: proyecto con solo espacios cuenta como en captura', async () => {
|
|
const clientes = [
|
|
{
|
|
id: 'c1',
|
|
nombreCompleto: 'Espacios',
|
|
proyecto: ' ',
|
|
unidadCodigo: null,
|
|
mondayApartadoId: null,
|
|
createdAt: new Date('2026-08-26T09:00:00Z'),
|
|
},
|
|
]
|
|
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], clientes, AHORA)
|
|
expect(r.misClientes[0]?.completado).toBe(false)
|
|
})
|
|
|
|
it('sin clientes: misClientes va vacío (no requiere Monday)', async () => {
|
|
const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], [], AHORA)
|
|
expect(r.misClientes).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('clasificadores de estado', () => {
|
|
it('etapas terminales de oportunidad (case-insensitive)', () => {
|
|
expect(esEtapaTerminal('GANADO')).toBe(true)
|
|
expect(esEtapaTerminal('perdido')).toBe(true)
|
|
expect(esEtapaTerminal('Abandono')).toBe(true)
|
|
expect(esEtapaTerminal('Afluencia')).toBe(false)
|
|
expect(esEtapaTerminal('Calificado')).toBe(false)
|
|
expect(esEtapaTerminal('Intento de contacto')).toBe(false)
|
|
})
|
|
|
|
it('estados terminales de lead (tablero Leads, case-insensitive)', () => {
|
|
expect(esEstadoTerminalLead('DESCARTADO')).toBe(true)
|
|
expect(esEstadoTerminalLead('calificado')).toBe(true)
|
|
expect(esEstadoTerminalLead('Abandono')).toBe(true)
|
|
expect(esEstadoTerminalLead('Nuevo')).toBe(false)
|
|
expect(esEstadoTerminalLead('Intento de contacto')).toBe(false)
|
|
})
|
|
|
|
it('estados finales de apartado', () => {
|
|
expect(esEstadoFinalApartado('Escriturado')).toBe(true)
|
|
expect(esEstadoFinalApartado('CANCELADO')).toBe(true)
|
|
expect(esEstadoFinalApartado('Pago Apartado')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('resumenMiDia — cuenta compartida / uso solo-app (sin Monday)', () => {
|
|
const COMPARTIDO = 999000111
|
|
|
|
it('sin vínculo Monday: ve lo que capturó en la app (registro local)', async () => {
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1' }), // capturado por mí (abajo)
|
|
lead({ id: 'l2' }), // no es mío
|
|
],
|
|
oportunidades: [oportunidad({ id: 'o1', updatedAt: '2026-08-28T10:00:00Z' })], // capturada por mí
|
|
})
|
|
const capturas = [
|
|
{ leadId: 'l1', nombreLead: 'Lead l1', telefono: null, createdAt: new Date('2026-08-27') },
|
|
{ leadId: 'o1', nombreLead: 'Lead o1', telefono: null, createdAt: new Date('2026-08-27') },
|
|
]
|
|
const r = await resumenMiDia(monday, sinMapeo, new Map(), capturas, [], AHORA)
|
|
expect(r.misLeads.map((l) => l.id)).toEqual(['o1', 'l1'])
|
|
expect(r.misOportunidades.items.map((o) => o.id)).toEqual(['o1'])
|
|
})
|
|
|
|
it('cuenta compartida: NO ve lo asignado a la cuenta compartida, solo su captura local', async () => {
|
|
const compartido: UsuarioMiDia = { nombre: 'Vendedor Piso', rol: 'vendedor', mondayUserId: COMPARTIDO }
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1', vendedorIds: [COMPARTIDO] }), // asignado a la cuenta compartida, capturado por otro
|
|
lead({ id: 'l2', vendedorIds: [COMPARTIDO] }), // asignado a la compartida, capturado por MÍ
|
|
],
|
|
})
|
|
const capturas = [{ leadId: 'l2', nombreLead: 'Lead l2', telefono: null, createdAt: new Date('2026-08-27') }]
|
|
const r = await resumenMiDia(monday, compartido, new Map(), capturas, [], AHORA, COMPARTIDO)
|
|
expect(r.misLeads.map((l) => l.id)).toEqual(['l2']) // l1 queda fuera
|
|
})
|
|
|
|
it('cuenta NO compartida: ve capturas + asignados (comportamiento de siempre)', async () => {
|
|
const monday = mondayMock({
|
|
leads: [
|
|
lead({ id: 'l1', vendedorIds: [MI_ID] }), // asignado a mí
|
|
lead({ id: 'l2', vendedorIds: [OTRO_ID] }), // capturado por mí (abajo)
|
|
lead({ id: 'l3', vendedorIds: [OTRO_ID] }), // ni asignado ni capturado
|
|
],
|
|
})
|
|
const capturas = [{ leadId: 'l2', nombreLead: 'Lead l2', telefono: null, createdAt: new Date('2026-08-27') }]
|
|
const r = await resumenMiDia(monday, vendedor, new Map(), capturas, [], AHORA, COMPARTIDO)
|
|
expect(r.misLeads.map((l) => l.id).sort()).toEqual(['l1', 'l2'])
|
|
})
|
|
|
|
it('mis apartados incluye los ligados a mis altas de cliente aunque el Vendedor sea la cuenta compartida', async () => {
|
|
const compartido: UsuarioMiDia = { nombre: 'Vendedor Piso', rol: 'vendedor', mondayUserId: COMPARTIDO }
|
|
const monday = mondayMock({
|
|
apartados: [
|
|
apartado({ id: 'p1', vendedorIds: [COMPARTIDO] }), // apartado de MI alta (abajo)
|
|
apartado({ id: 'p2', vendedorIds: [COMPARTIDO] }), // apartado de otro
|
|
],
|
|
})
|
|
const clientes = [
|
|
{ id: 'c1', nombreCompleto: 'Cliente p1', proyecto: 'SENDA', unidadCodigo: 'U-1', mondayApartadoId: 'p1', createdAt: new Date('2026-08-27') },
|
|
]
|
|
const r = await resumenMiDia(monday, compartido, new Map(), [], clientes, AHORA, COMPARTIDO)
|
|
expect(r.misApartados.map((a) => a.id)).toEqual(['p1'])
|
|
})
|
|
})
|