Nuevas tablas: - ActividadLocal (historial de llamadas, reuniones, tareas) - AsignacionLocal (round-robin local) - fechaLimite en LeadCaptura Servicios nuevos: - actividad-local.service.ts (CRUD actividades) - local-canal.ts (canal de envío para recordatorios) - seed-modo-local.js (activar modo local) Mi día local mejorado: - Mis actividades hoy (filtros usuario/fecha/estado) - Apartados por vencer (con fechaLimite real) - Equipo (contadores por vendedor, solo coordinador/admin) - unidadCodigo en oportunidades desde datosJson Automatizaciones locales: - Round-robin: asigna leads sin vendedor + crea actividad - Inactividad: detecta oportunidades >10 días sin update - Escalado: alerta oportunidades >48h sin contacto Reportes: actividadesAbiertas + parcial:false en modo local Cron wiring: crons locales cuando Monday caído 661 tests verdes.
234 lines
9.1 KiB
TypeScript
234 lines
9.1 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
import { ActividadLocalService } from './actividad-local.service.js'
|
|
|
|
// Mock de Prisma: solo operaciones de CRUD sobre ActividadLocal
|
|
const mockActividades: Record<string, {
|
|
id: string; titulo: string; descripcion: string | null; usuarioId: string
|
|
leadCapturaId: string | null; fechaVencimiento: Date | null; fechaCierre: Date | null
|
|
estado: string; tipo: string; createdAt: Date; updatedAt: Date
|
|
}> = {}
|
|
let consecutivo = 0
|
|
|
|
vi.mock('./prisma.js', () => ({
|
|
prisma: {
|
|
actividadLocal: {
|
|
create: vi.fn(async ({ data }: { data: Record<string, unknown> }) => {
|
|
consecutivo++
|
|
const id = `act_${consecutivo}`
|
|
const fila = {
|
|
id,
|
|
titulo: data.titulo as string,
|
|
descripcion: (data.descripcion as string) ?? null,
|
|
usuarioId: data.usuarioId as string,
|
|
leadCapturaId: (data.leadCapturaId as string) ?? null,
|
|
fechaVencimiento: (data.fechaVencimiento as Date) ?? null,
|
|
fechaCierre: null,
|
|
estado: (data.estado as string) ?? 'ABIERTA',
|
|
tipo: (data.tipo as string) ?? 'SEGUIMIENTO',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
}
|
|
mockActividades[id] = fila
|
|
return fila
|
|
}),
|
|
findMany: vi.fn(async ({ where }: { where: Record<string, unknown> }) => {
|
|
let filas = Object.values(mockActividades)
|
|
if (where.usuarioId !== undefined) {
|
|
filas = filas.filter((f) => f.usuarioId === where.usuarioId)
|
|
}
|
|
if (where.estado !== undefined) {
|
|
filas = filas.filter((f) => f.estado === where.estado)
|
|
}
|
|
if (where.fechaVencimiento !== undefined && typeof where.fechaVencimiento === 'object') {
|
|
const fv = where.fechaVencimiento as Record<string, Date>
|
|
if (fv.gte !== undefined) filas = filas.filter((f) => f.fechaVencimiento !== null && f.fechaVencimiento >= fv.gte)
|
|
if (fv.lt !== undefined) filas = filas.filter((f) => f.fechaVencimiento !== null && f.fechaVencimiento < fv.lt)
|
|
}
|
|
return filas
|
|
}),
|
|
count: vi.fn(async ({ where }: { where: Record<string, unknown> }) => {
|
|
let filas = Object.values(mockActividades)
|
|
if (where.usuarioId !== undefined) {
|
|
filas = filas.filter((f) => f.usuarioId === where.usuarioId)
|
|
}
|
|
if (where.estado !== undefined) {
|
|
filas = filas.filter((f) => f.estado === where.estado)
|
|
}
|
|
if (where.fechaVencimiento !== undefined && typeof where.fechaVencimiento === 'object') {
|
|
const fv = where.fechaVencimiento as Record<string, Date>
|
|
if (fv.gte !== undefined && fv.lt !== undefined) {
|
|
filas = filas.filter((f) => f.fechaVencimiento !== null && f.fechaVencimiento >= fv.gte && f.fechaVencimiento < fv.lt)
|
|
}
|
|
}
|
|
return filas.length
|
|
}),
|
|
findUnique: vi.fn(async ({ where }: { where: { id: string } }) => mockActividades[where.id] ?? null),
|
|
update: vi.fn(async ({ where, data }: { where: { id: string }; data: Record<string, unknown> }) => {
|
|
const fila = mockActividades[where.id]
|
|
if (!fila) throw new Error('Not found')
|
|
Object.assign(fila, data, { updatedAt: new Date() })
|
|
return fila
|
|
}),
|
|
delete: vi.fn(async ({ where }: { where: { id: string } }) => {
|
|
const fila = mockActividades[where.id]
|
|
if (!fila) throw new Error('Not found')
|
|
delete mockActividades[where.id]
|
|
return fila
|
|
}),
|
|
},
|
|
},
|
|
}))
|
|
|
|
beforeEach(() => {
|
|
Object.keys(mockActividades).forEach((k) => delete mockActividades[k])
|
|
consecutivo = 0
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('ActividadLocalService', () => {
|
|
const svc = new ActividadLocalService()
|
|
|
|
it('crearActividad crea una actividad con valores por defecto', async () => {
|
|
const act = await svc.crearActividad({ titulo: 'Llamar a Juan', usuarioId: 'u1' })
|
|
expect(act.id).toBe('act_1')
|
|
expect(act.titulo).toBe('Llamar a Juan')
|
|
expect(act.estado).toBe('ABIERTA')
|
|
expect(act.tipo).toBe('SEGUIMIENTO')
|
|
expect(act.descripcion).toBeNull()
|
|
expect(act.leadCapturaId).toBeNull()
|
|
})
|
|
|
|
it('crearActividad con campos opcionales', async () => {
|
|
const fv = new Date('2026-09-10T10:00:00')
|
|
const act = await svc.crearActividad({
|
|
titulo: 'Reunión',
|
|
descripcion: 'Seguimiento del lead',
|
|
usuarioId: 'u2',
|
|
leadCapturaId: 'loc_123',
|
|
fechaVencimiento: fv,
|
|
tipo: 'REUNION',
|
|
})
|
|
expect(act.descripcion).toBe('Seguimiento del lead')
|
|
expect(act.leadCapturaId).toBe('loc_123')
|
|
expect(act.fechaVencimiento).toBe(fv)
|
|
expect(act.tipo).toBe('REUNION')
|
|
})
|
|
|
|
it('listarActividades filtra por usuario', async () => {
|
|
await svc.crearActividad({ titulo: 'A1', usuarioId: 'u1' })
|
|
await svc.crearActividad({ titulo: 'A2', usuarioId: 'u2' })
|
|
await svc.crearActividad({ titulo: 'A3', usuarioId: 'u1' })
|
|
|
|
const lista = await svc.listarActividades('u1')
|
|
expect(lista).toHaveLength(2)
|
|
expect(lista.every((a) => a.usuarioId === 'u1')).toBe(true)
|
|
})
|
|
|
|
it('listarActividades con soloAbiertas', async () => {
|
|
await svc.crearActividad({ titulo: 'Abierta', usuarioId: 'u1' })
|
|
const cerrada = await svc.crearActividad({ titulo: 'Cerrada', usuarioId: 'u1' })
|
|
await svc.cerrarActividad(cerrada.id)
|
|
|
|
const abiertas = await svc.listarActividades('u1', { soloAbiertas: true })
|
|
expect(abiertas).toHaveLength(1)
|
|
expect(abiertas[0].titulo).toBe('Abierta')
|
|
})
|
|
|
|
it('cerrarActividad marca estado CERRADA y fechaCierre', async () => {
|
|
const act = await svc.crearActividad({ titulo: 'Test', usuarioId: 'u1' })
|
|
const cerrada = await svc.cerrarActividad(act.id)
|
|
expect(cerrada.estado).toBe('CERRADA')
|
|
expect(cerrada.fechaCierre).toBeInstanceOf(Date)
|
|
})
|
|
|
|
it('cerrarActividad lanza error si no existe', async () => {
|
|
await expect(svc.cerrarActividad('no_existe')).rejects.toThrow('no encontrada')
|
|
})
|
|
|
|
it('eliminarActividad borra la actividad', async () => {
|
|
const act = await svc.crearActividad({ titulo: 'Borrar', usuarioId: 'u1' })
|
|
await svc.eliminarActividad(act.id)
|
|
const todas = await svc.listarActividades('u1')
|
|
expect(todas).toHaveLength(0)
|
|
})
|
|
|
|
it('eliminarActividad lanza error si no existe', async () => {
|
|
await expect(svc.eliminarActividad('no_existe')).rejects.toThrow('no encontrada')
|
|
})
|
|
|
|
it('contarActividadesAbiertas sin filtro de usuario', async () => {
|
|
await svc.crearActividad({ titulo: 'A1', usuarioId: 'u1' })
|
|
await svc.crearActividad({ titulo: 'A2', usuarioId: 'u2' })
|
|
expect(await svc.contarActividadesAbiertas()).toBe(2)
|
|
})
|
|
|
|
it('contarActividadesAbiertas con filtro de usuario', async () => {
|
|
await svc.crearActividad({ titulo: 'A1', usuarioId: 'u1' })
|
|
await svc.crearActividad({ titulo: 'A2', usuarioId: 'u2' })
|
|
expect(await svc.contarActividadesAbiertas('u1')).toBe(1)
|
|
})
|
|
|
|
it('contarActividadesHoy cuenta solo las de hoy', async () => {
|
|
const hoy = new Date('2026-09-07T10:00:00')
|
|
const ayer = new Date('2026-09-06T10:00:00')
|
|
await svc.crearActividad({ titulo: 'Hoy', usuarioId: 'u1', fechaVencimiento: hoy })
|
|
await svc.crearActividad({ titulo: 'Ayer', usuarioId: 'u1', fechaVencimiento: ayer })
|
|
|
|
// contarActividadesHoy usa new Date() internamente, mockeamos el Date global
|
|
const DateOrig = Date
|
|
vi.stubGlobal('Date', class extends DateOrig {
|
|
constructor(...args: unknown[]) {
|
|
if (args.length === 0) super('2026-09-07T12:00:00')
|
|
else super(...args)
|
|
}
|
|
})
|
|
|
|
const count = await svc.contarActividadesHoy('u1')
|
|
expect(count).toBe(1)
|
|
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('actividadesVencidas retorna solo las que vencieron y están abiertas', async () => {
|
|
const pasada = new Date('2026-09-06T10:00:00')
|
|
const futura = new Date('2026-09-10T10:00:00')
|
|
await svc.crearActividad({ titulo: 'Pasada', usuarioId: 'u1', fechaVencimiento: pasada })
|
|
await svc.crearActividad({ titulo: 'Futura', usuarioId: 'u1', fechaVencimiento: futura })
|
|
|
|
// actividadesVencidas compara con new Date() → 2026-09-07T12:00:00
|
|
const DateOrig = Date
|
|
vi.stubGlobal('Date', class extends DateOrig {
|
|
constructor(...args: unknown[]) {
|
|
if (args.length === 0) super('2026-09-07T12:00:00')
|
|
else super(...args)
|
|
}
|
|
})
|
|
|
|
const vencidas = await svc.actividadesVencidas('u1')
|
|
expect(vencidas).toHaveLength(1)
|
|
expect(vencidas[0].titulo).toBe('Pasada')
|
|
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
it('actividadesVencidas con filtro de usuario', async () => {
|
|
const pasada = new Date('2026-09-06T10:00:00')
|
|
await svc.crearActividad({ titulo: 'U1', usuarioId: 'u1', fechaVencimiento: pasada })
|
|
await svc.crearActividad({ titulo: 'U2', usuarioId: 'u2', fechaVencimiento: pasada })
|
|
|
|
const DateOrig = Date
|
|
vi.stubGlobal('Date', class extends DateOrig {
|
|
constructor(...args: unknown[]) {
|
|
if (args.length === 0) super('2026-09-07T12:00:00')
|
|
else super(...args)
|
|
}
|
|
})
|
|
|
|
const vencidas = await svc.actividadesVencidas('u1')
|
|
expect(vencidas).toHaveLength(1)
|
|
expect(vencidas[0].titulo).toBe('U1')
|
|
|
|
vi.unstubAllGlobals()
|
|
})
|
|
})
|