feat(leads): doble via de captura — lead rapido a Leads, afluencia a Oportunidades
- POST /api/leads: captura ligera al tablero Leads (sin oportunidad), self-assign a Vendedor o round-robin; tambien cubre la captura manual desde la UI de Monday (webhook create_item re-registrado). - POST /api/afluencias (nuevo): la forma completa sigue cayendo en Oportunidades con todo el perfil (etapa Afluencia, columna Asesor). - Round-robin dual (ambos tableros), Mi dia mezcla leads+oportunidades con insignia de origen, acciones con parametro fuente, reportes: leads desde Leads, perfilamiento desde Oportunidades, funnel Leads->Afluencia->...->Ganado.
This commit is contained in:
@@ -1,11 +1,27 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { construirResumen, ReportesService, type DatosResumen } from './reportes.service.js'
|
||||
import type { OportunidadMonday, Unidad } from './monday.service.js'
|
||||
import type { LeadMonday, OportunidadMonday, Unidad } from './monday.service.js'
|
||||
import type { PlanPago } from './cobranza.service.js'
|
||||
|
||||
// ---------- Fixtures ----------
|
||||
|
||||
/** Afluencia/oportunidad del board Oportunidades (fuente única de la sección leads) */
|
||||
/** Lead del tablero Leads (captura ligera; fuente de la sección leads) */
|
||||
function lead(id: string, overrides: Partial<LeadMonday> = {}): LeadMonday {
|
||||
return {
|
||||
id,
|
||||
nombre: `Lead ${id}`,
|
||||
createdAt: '2026-08-20T10:00:00Z',
|
||||
updatedAt: '2026-08-20T10:00:00Z',
|
||||
estado: 'Nuevo',
|
||||
origen: 'Facebook',
|
||||
vendedorIds: [],
|
||||
telefono: '',
|
||||
email: '',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/** Afluencia del board Oportunidades (fuente del perfilamiento de afluencia) */
|
||||
function afluencia(id: string, overrides: Partial<OportunidadMonday> = {}): OportunidadMonday {
|
||||
return {
|
||||
id,
|
||||
@@ -68,37 +84,37 @@ function pago(id: string, fechaVencimiento: string, monto: number, montoPagado =
|
||||
|
||||
const AHORA = new Date('2026-08-22T12:00:00') // mediodía local
|
||||
|
||||
const DATOS_BASE: DatosResumen = { oportunidades: [], unidades: [], planes: [], actividadesAbiertas: 0 }
|
||||
const DATOS_BASE: DatosResumen = { leads: [], oportunidades: [], unidades: [], planes: [], actividadesAbiertas: 0 }
|
||||
|
||||
// ---------- construirResumen (puro) ----------
|
||||
|
||||
describe('construirResumen', () => {
|
||||
it('cuenta la afluencia por estado (Etapa) y por origen (Origen del Lead)', () => {
|
||||
it('cuenta leads por estado y por origen (tablero Leads, captura ligera)', () => {
|
||||
const r = construirResumen(
|
||||
{
|
||||
...DATOS_BASE,
|
||||
oportunidades: [
|
||||
afluencia('1', { etapa: 'Afluencia', origen: 'Facebook' }),
|
||||
afluencia('2', { etapa: 'Afluencia', origen: 'Facebook' }),
|
||||
afluencia('3', { etapa: 'Contactado', origen: 'Referido' }),
|
||||
afluencia('4', { etapa: '', origen: '' }),
|
||||
leads: [
|
||||
lead('1', { estado: 'Nuevo', origen: 'Facebook' }),
|
||||
lead('2', { estado: 'Nuevo', origen: 'Facebook' }),
|
||||
lead('3', { estado: 'Contactado', origen: 'Referido' }),
|
||||
lead('4', { estado: '', origen: '' }),
|
||||
],
|
||||
},
|
||||
AHORA,
|
||||
)
|
||||
expect(r.leads.total).toBe(4)
|
||||
expect(r.leads.porEstado).toEqual({ Afluencia: 2, Contactado: 1, '(sin dato)': 1 })
|
||||
expect(r.leads.porEstado).toEqual({ Nuevo: 2, Contactado: 1, '(sin dato)': 1 })
|
||||
expect(r.leads.porOrigen).toEqual({ Facebook: 2, Referido: 1, '(sin dato)': 1 })
|
||||
})
|
||||
|
||||
it('cuenta afluencias nuevas de hoy y de los últimos 7 días (fecha local)', () => {
|
||||
it('cuenta leads nuevos de hoy y de los últimos 7 días (fecha local)', () => {
|
||||
const r = construirResumen(
|
||||
{
|
||||
...DATOS_BASE,
|
||||
oportunidades: [
|
||||
afluencia('1', { createdAt: '2026-08-22T08:00:00' }), // hoy
|
||||
afluencia('2', { createdAt: '2026-08-18T23:00:00' }), // esta semana
|
||||
afluencia('3', { createdAt: '2026-08-10T12:00:00' }), // hace 12 días
|
||||
leads: [
|
||||
lead('1', { createdAt: '2026-08-22T08:00:00' }), // hoy
|
||||
lead('2', { createdAt: '2026-08-18T23:00:00' }), // esta semana
|
||||
lead('3', { createdAt: '2026-08-10T12:00:00' }), // hace 12 días
|
||||
],
|
||||
},
|
||||
AHORA,
|
||||
@@ -300,7 +316,11 @@ describe('construirResumen', () => {
|
||||
describe('ReportesService', () => {
|
||||
function mondayMock() {
|
||||
return {
|
||||
llamadas: { oportunidades: 0, unidades: 0, actividades: 0 },
|
||||
llamadas: { leads: 0, oportunidades: 0, unidades: 0, actividades: 0 },
|
||||
async getLeads(): Promise<LeadMonday[]> {
|
||||
this.llamadas.leads++
|
||||
return [lead('1')]
|
||||
},
|
||||
async getOportunidades(): Promise<OportunidadMonday[]> {
|
||||
this.llamadas.oportunidades++
|
||||
return [afluencia('1')]
|
||||
@@ -316,16 +336,17 @@ describe('ReportesService', () => {
|
||||
}
|
||||
}
|
||||
|
||||
it('agrega todas las fuentes en una sola respuesta (una sola llamada a Oportunidades)', async () => {
|
||||
it('agrega todas las fuentes en una sola respuesta', async () => {
|
||||
const monday = mondayMock()
|
||||
const svc = new ReportesService(monday, async () => [plan('a', { pagos: [pago('p1', '2026-09-01', 50000)] })])
|
||||
const r = await svc.resumen()
|
||||
expect(r.leads.total).toBe(1) // la sección leads se alimenta de Oportunidades
|
||||
expect(r.leads.total).toBe(1) // la sección leads se alimenta del tablero Leads
|
||||
expect(r.oportunidades.total).toBe(1)
|
||||
expect(r.unidades.total).toBe(1)
|
||||
expect(r.cobranza.planes.activos).toBe(1)
|
||||
expect(r.actividadesAbiertas).toBe(7)
|
||||
expect(monday.llamadas.oportunidades).toBe(1) // sin llamada duplicada
|
||||
expect(monday.llamadas.leads).toBe(1)
|
||||
expect(monday.llamadas.oportunidades).toBe(1)
|
||||
})
|
||||
|
||||
it('cachea 60 s: la segunda llamada no vuelve a Monday ni a Prisma', async () => {
|
||||
|
||||
Reference in New Issue
Block a user