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.
132 lines
4.0 KiB
TypeScript
132 lines
4.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
cliente: { findFirst: vi.fn() },
|
|
leadCaptura: { findUnique: vi.fn() },
|
|
planPago: { findMany: vi.fn() },
|
|
cotizacion: { findMany: vi.fn() },
|
|
documento: { findMany: vi.fn() },
|
|
actividadLocal: { findFirst: vi.fn() },
|
|
}))
|
|
|
|
vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
|
|
|
// Mock lead-scoring to avoid circular dependency
|
|
vi.mock('../services/lead-scoring.service.js', () => ({
|
|
calcularScore: vi.fn(() => 75),
|
|
}))
|
|
|
|
import { perfilCompleto } from './contactos-detalle.service.js'
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
prismaMock.cliente.findFirst.mockResolvedValue(null)
|
|
prismaMock.leadCaptura.findUnique.mockResolvedValue(null)
|
|
prismaMock.planPago.findMany.mockResolvedValue([])
|
|
prismaMock.cotizacion.findMany.mockResolvedValue([])
|
|
prismaMock.documento.findMany.mockResolvedValue([])
|
|
prismaMock.actividadLocal.findFirst.mockResolvedValue(null)
|
|
})
|
|
|
|
describe('perfilCompleto', () => {
|
|
it('devuelve null si el contacto no existe', async () => {
|
|
const result = await perfilCompleto('no-existe')
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('arma perfil completo desde Cliente', async () => {
|
|
prismaMock.cliente.findFirst.mockResolvedValue({
|
|
id: 'c1',
|
|
nombreCompleto: 'Juan Pérez',
|
|
curp: 'CURP123',
|
|
rfc: 'RFC123',
|
|
email: 'juan@test.com',
|
|
celular: '4771234567',
|
|
proyecto: 'SENDA',
|
|
unidadCodigo: 'SENDA-101',
|
|
mondayContactoId: null,
|
|
createdAt: new Date('2026-09-01'),
|
|
})
|
|
|
|
prismaMock.planPago.findMany.mockResolvedValue([
|
|
{
|
|
id: 'plan1',
|
|
clienteNombre: 'Juan Pérez',
|
|
unidadCodigo: 'SENDA-101',
|
|
proyecto: 'SENDA',
|
|
pagos: [
|
|
{ numPago: 1, fechaVencimiento: new Date('2026-10-01'), monto: 5000, montoPagado: 0 },
|
|
],
|
|
},
|
|
])
|
|
|
|
prismaMock.documento.findMany.mockResolvedValue([
|
|
{ id: 'd1', tipo: 'INE', nombreArchivo: 'ine.pdf', createdAt: new Date('2026-09-02') },
|
|
])
|
|
|
|
const result = await perfilCompleto('c1')
|
|
expect(result).not.toBeNull()
|
|
expect(result?.contacto['nombre']).toBe('Juan Pérez')
|
|
expect(result?.contacto['CURP']).toBe('CURP123')
|
|
expect(result?.historialPagos).toHaveLength(1)
|
|
expect(result?.historialPagos[0]?.estado).toBe('pendiente')
|
|
expect(result?.documentos).toHaveLength(1)
|
|
expect(result?.documentos[0]?.tipo).toBe('INE')
|
|
})
|
|
|
|
it('arma perfil desde LeadCaptura cuando no hay Cliente', async () => {
|
|
prismaMock.leadCaptura.findUnique.mockResolvedValue({
|
|
leadId: 'loc_1',
|
|
nombreLead: 'Ana López',
|
|
telefono: '6649998877',
|
|
email: 'ana@test.com',
|
|
origen: 'Facebook',
|
|
etapa: 'INTERESADO',
|
|
createdAt: new Date('2026-09-05'),
|
|
updatedAt: new Date('2026-09-07'),
|
|
})
|
|
|
|
prismaMock.actividadLocal.findFirst.mockResolvedValue({
|
|
titulo: 'Llamar a Ana',
|
|
tipo: 'SEGUIMIENTO',
|
|
createdAt: new Date('2026-09-07'),
|
|
})
|
|
|
|
const result = await perfilCompleto('loc_1')
|
|
expect(result).not.toBeNull()
|
|
expect(result?.contacto['nombre']).toBe('Ana López')
|
|
expect(result?.contacto['Etapa']).toBe('INTERESADO')
|
|
expect(result?.leadScore).toBe(75)
|
|
expect(result?.ultimaActividad?.titulo).toBe('Llamar a Ana')
|
|
})
|
|
|
|
it('cotizaciones se incluyen cuando hay teléfono', async () => {
|
|
prismaMock.cliente.findFirst.mockResolvedValue({
|
|
id: 'c1',
|
|
nombreCompleto: 'Test',
|
|
curp: '',
|
|
rfc: '',
|
|
email: null,
|
|
celular: '4771234567',
|
|
proyecto: 'A',
|
|
unidadCodigo: null,
|
|
mondayContactoId: null,
|
|
createdAt: new Date(),
|
|
})
|
|
|
|
prismaMock.cotizacion.findMany.mockResolvedValue([
|
|
{
|
|
id: 'cot1',
|
|
unidadCodigo: 'U-101',
|
|
tipoPropuesta: 'BANCARIO',
|
|
precioFinal: 3000000,
|
|
createdAt: new Date('2026-09-01'),
|
|
},
|
|
])
|
|
|
|
const result = await perfilCompleto('c1')
|
|
expect(result?.cotizaciones).toHaveLength(1)
|
|
expect(result?.cotizaciones[0]?.tipoPropuesta).toBe('BANCARIO')
|
|
})
|
|
})
|