Backend (10 endpoints nuevos): - Recordatorios personales (CRUD + tabla RecordatorioVendedor) - Lead scoring 0-100 (etapa+recencia+datos) - Dashboard mora (pagos vencidos, monto, días atraso) - Timeline de pagos (pagado/vence/futuro) - Perfil completo contacto (pagos+cotizaciones+docs+score) - Exportar contactos a CSV Frontend Mi día: - Resumen matutino (saludo+KPIs) - Historial visual lead (timeline) - Score badge 0-100 (verde/amarillo/rojo) - Vista kanban (drag & drop) Frontend Cobranza: - Dashboard mora (tabla+WhatsApp) - Timeline pagos visual (nodos coloreados) Frontend Alta Clientes: - Checklist documentos visual - Foto cliente (cámara) Frontend Landing: - Perfil contacto completo - Exportar contactos CSV Frontend Mi día (extras): - Comparativa mensual - KPIs dashboard (leads hoy, ventas, conversión) 806 tests, 5 apps construidas.
144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
leadCaptura: {
|
|
findMany: vi.fn(),
|
|
},
|
|
}))
|
|
|
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
|
|
|
import { calcularScore, obtenerScores } from './lead-scoring.service.js'
|
|
|
|
describe('calcularScore', () => {
|
|
const ahora = new Date('2026-09-08T12:00:00Z')
|
|
|
|
it('asigna score base según etapa (NUEVO = 20)', () => {
|
|
const lead = {
|
|
etapa: 'NUEVO',
|
|
telefono: null,
|
|
email: null,
|
|
origen: null,
|
|
createdAt: new Date('2026-09-07T12:00:00Z'),
|
|
updatedAt: new Date('2026-09-07T12:00:00Z'),
|
|
}
|
|
// 20 (etapa) + 5 (recencia <7d) = 25
|
|
expect(calcularScore(lead, ahora)).toBe(25)
|
|
})
|
|
|
|
it('asigna score CALIFICADO (90)', () => {
|
|
const lead = {
|
|
etapa: 'CALIFICADO',
|
|
telefono: '4771234567',
|
|
email: 'test@email.com',
|
|
origen: 'Facebook',
|
|
createdAt: new Date('2026-09-08T00:00:00Z'),
|
|
updatedAt: new Date('2026-09-08T00:00:00Z'),
|
|
}
|
|
// 90 + 10 (<24h) + 5 tel + 5 email + 5 origen = 115 → clamp 100
|
|
expect(calcularScore(lead, ahora)).toBe(100)
|
|
})
|
|
|
|
it('aplica penalización >30 días sin actividad', () => {
|
|
const lead = {
|
|
etapa: 'CONTACTADO',
|
|
telefono: null,
|
|
email: null,
|
|
origen: null,
|
|
createdAt: new Date('2026-07-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-07-01T00:00:00Z'),
|
|
}
|
|
// 40 (CONTACTADO) + (-20) (>30d) = 20
|
|
expect(calcularScore(lead, ahora)).toBe(20)
|
|
})
|
|
|
|
it('score mínimo es 0', () => {
|
|
const lead = {
|
|
etapa: 'DESCARTADO',
|
|
telefono: null,
|
|
email: null,
|
|
origen: null,
|
|
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
}
|
|
// 10 (desconocida) + (-20) = -10 → clamp 0
|
|
expect(calcularScore(lead, ahora)).toBe(0)
|
|
})
|
|
|
|
it('recencia <24h suma +10', () => {
|
|
const lead = {
|
|
etapa: 'NUEVO',
|
|
telefono: '1234567890',
|
|
email: 'a@b.com',
|
|
origen: 'Web',
|
|
createdAt: new Date('2026-09-08T10:00:00Z'),
|
|
updatedAt: new Date('2026-09-08T10:00:00Z'),
|
|
}
|
|
// 20 + 10 + 5 + 5 + 5 = 45
|
|
expect(calcularScore(lead, ahora)).toBe(45)
|
|
})
|
|
|
|
it('INTERESADO = 60 + recencia <7d = +5 = 65', () => {
|
|
const lead = {
|
|
etapa: 'INTERESADO',
|
|
telefono: null,
|
|
email: null,
|
|
origen: null,
|
|
createdAt: new Date('2026-09-05T12:00:00Z'),
|
|
updatedAt: new Date('2026-09-05T12:00:00Z'),
|
|
}
|
|
expect(calcularScore(lead, ahora)).toBe(65)
|
|
})
|
|
})
|
|
|
|
describe('obtenerScores', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('devuelve scores ordenados de mayor a menor', async () => {
|
|
prismaMock.leadCaptura.findMany.mockResolvedValue([
|
|
{
|
|
leadId: 'l1',
|
|
nombreLead: 'A',
|
|
usuarioId: 'u1',
|
|
etapa: 'NUEVO',
|
|
telefono: null,
|
|
email: null,
|
|
origen: null,
|
|
createdAt: new Date('2026-09-07'),
|
|
updatedAt: new Date('2026-09-07'),
|
|
},
|
|
{
|
|
leadId: 'l2',
|
|
nombreLead: 'B',
|
|
usuarioId: 'u1',
|
|
etapa: 'CALIFICADO',
|
|
telefono: '1234567890',
|
|
email: 'b@b.com',
|
|
origen: 'Web',
|
|
createdAt: new Date('2026-09-08'),
|
|
updatedAt: new Date('2026-09-08'),
|
|
},
|
|
])
|
|
|
|
const scores = await obtenerScores()
|
|
expect(scores).toHaveLength(2)
|
|
expect(scores[0]!.leadId).toBe('l2')
|
|
expect(scores[0]!.score).toBeGreaterThan(scores[1]!.score)
|
|
})
|
|
|
|
it('filtra por usuarioId cuando se proporciona', async () => {
|
|
prismaMock.leadCaptura.findMany.mockResolvedValue([])
|
|
|
|
await obtenerScores('u1')
|
|
|
|
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
board: { in: ['LEADS', 'OPORTUNIDADES'] },
|
|
usuarioId: 'u1',
|
|
},
|
|
})
|
|
})
|
|
})
|