feat(ux): 17 mejoras de experiencia de usuario
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.
This commit is contained in:
143
api-gateway/src/services/lead-scoring.service.test.ts
Normal file
143
api-gateway/src/services/lead-scoring.service.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user