feat(leads): la captura desde una app se asigna al capturista
POST /api/leads con sesion y Monday vinculado asigna el lead a quien lo registro (mas su actividad de contacto) en vez del round-robin; sin vinculo o sin token sigue el reparto. Bandera rr:skip para que el webhook create_item no pise la asignacion durante la propagacion de Monday (la carrera se observo en el smoke). Mensajes de exito en /hoy y /client reflejan a quien quedo asignado.
This commit is contained in:
@@ -225,6 +225,17 @@ describe('AutomatizacionesService.runRoundRobin', () => {
|
||||
expect(monday.asignaciones).toEqual([{ itemId: '1', userId: 101 }])
|
||||
})
|
||||
|
||||
it('omite leads con bandera rr:skip=capturista (los asigna su capturista)', async () => {
|
||||
const monday = mondayMock([lead('1'), lead('2')])
|
||||
const state = stateEnMemoria()
|
||||
state.mapa.set('rr:skip:1', 'capturista') // el capturista lo asigna
|
||||
state.mapa.set('rr:skip:2', 'round-robin') // bandera liberada: sí entra al reparto
|
||||
const svc = crearServicio(monday, state)
|
||||
const r = await svc.runRoundRobin()
|
||||
expect(r.asignados).toBe(1)
|
||||
expect(monday.asignaciones).toEqual([{ itemId: '2', userId: 101 }])
|
||||
})
|
||||
|
||||
it('con soloItemId solo evalúa ese item (ruta del webhook create_item)', async () => {
|
||||
const monday = mondayMock([lead('1'), lead('2')])
|
||||
const svc = crearServicio(monday, stateEnMemoria())
|
||||
@@ -326,6 +337,31 @@ describe('Actividades automáticas (ACTIVIDADES_ENABLED)', () => {
|
||||
expect(monday.actividades).toEqual([])
|
||||
})
|
||||
|
||||
it('asignarACapturista: asigna al capturista y le crea su actividad de contacto', async () => {
|
||||
const monday = mondayMock([])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: true })
|
||||
|
||||
await svc.asignarACapturista('lead-77', 'Lead Propio', 555)
|
||||
|
||||
expect(monday.asignaciones).toEqual([{ itemId: 'lead-77', userId: 555 }])
|
||||
expect(monday.actividades[0]).toMatchObject({
|
||||
nombre: 'Contactar a Lead Propio',
|
||||
responsableId: 555,
|
||||
estadoLabel: 'Abierto',
|
||||
leadItemId: 'lead-77',
|
||||
})
|
||||
})
|
||||
|
||||
it('asignarACapturista con actividadesEnabled=false solo asigna', async () => {
|
||||
const monday = mondayMock([])
|
||||
const svc = new AutomatizacionesService(monday, stateEnMemoria(), { actividadesEnabled: false })
|
||||
|
||||
await svc.asignarACapturista('lead-78', 'Lead Propio', 555)
|
||||
|
||||
expect(monday.asignaciones).toEqual([{ itemId: 'lead-78', userId: 555 }])
|
||||
expect(monday.actividades).toEqual([])
|
||||
})
|
||||
|
||||
it('inactividad: crea "Retomar contacto" al vendedor asignado; sin vendedor no crea nada', async () => {
|
||||
const monday = mondayMock([
|
||||
lead('1', { updatedAt: '2026-08-01T00:00:00Z', vendedorIds: [103] }), // inactivo con vendedor
|
||||
|
||||
@@ -29,6 +29,8 @@ export const ETAPA_GANADO = 'GANADO'
|
||||
const CLAVE_RR_INDICE = 'rr:indice'
|
||||
const CLAVE_RR_VENDEDORES = 'rr:vendedores'
|
||||
const CLAVE_RR_ULTIMA_PASADA = 'rr:ultimaPasada'
|
||||
/** Prefijo de la bandera "este lead lo asigna su capturista, no el round-robin" */
|
||||
export const CLAVE_RR_SKIP = 'rr:skip:'
|
||||
const CLAVE_INACTIVIDAD_ULTIMO_ESCANEO = 'inactividad:ultimoEscaneo'
|
||||
const CLAVE_ESCALADO_ULTIMO_ESCANEO = 'escalado:ultimoEscaneo'
|
||||
const CLAVE_OPORTUNIDADES_ULTIMO_ESCANEO = 'oportunidades:ultimoEscaneo'
|
||||
@@ -270,12 +272,19 @@ export class AutomatizacionesService {
|
||||
*/
|
||||
async runRoundRobin(soloItemId?: string): Promise<ResultadoRoundRobin> {
|
||||
const leads = await this.monday.getLeads()
|
||||
const pendientes = leads.filter(
|
||||
const sinAsignar = leads.filter(
|
||||
(l) =>
|
||||
l.vendedorIds.length === 0 &&
|
||||
!esEstadoTerminal(l.estado) &&
|
||||
(soloItemId === undefined || l.id === soloItemId),
|
||||
)
|
||||
// Los leads capturados desde una app con sesión se asignan a su capturista
|
||||
// (bandera rr:skip='capturista'): sin este filtro, la latencia de Monday
|
||||
// hacía que el webhook los viera sin vendedor y pisara la asignación.
|
||||
const pendientes: typeof sinAsignar = []
|
||||
for (const l of sinAsignar) {
|
||||
if ((await this.state.get(`${CLAVE_RR_SKIP}${l.id}`)) !== 'capturista') pendientes.push(l)
|
||||
}
|
||||
if (pendientes.length === 0) {
|
||||
await this.state.set(CLAVE_RR_ULTIMA_PASADA, new Date().toISOString())
|
||||
return { asignados: 0, detalle: [] }
|
||||
@@ -306,6 +315,23 @@ export class AutomatizacionesService {
|
||||
return { asignados: detalle.length, detalle }
|
||||
}
|
||||
|
||||
/**
|
||||
* Asigna el lead a quien lo capturó desde una app del ecosistema (su usuario
|
||||
* Monday vinculado) y le crea la actividad de contacto — el mismo paquete
|
||||
* que haría el round-robin, pero para el capturista. No hay carrera con el
|
||||
* webhook create_item: runRoundRobin solo toma leads sin persona asignada.
|
||||
*/
|
||||
async asignarACapturista(leadId: string, leadNombre: string, vendedorId: number): Promise<void> {
|
||||
await this.monday.asignarVendedorALead(leadId, vendedorId)
|
||||
if (this.actividadesEnabled) {
|
||||
await this.crearActividadSegura({
|
||||
nombre: `Contactar a ${leadNombre}`,
|
||||
responsableId: vendedorId,
|
||||
leadItemId: leadId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escaneo de inactividad. Con `preview: true` no escribe nada en Monday ni en
|
||||
* el estado (sirve para el endpoint de preview).
|
||||
|
||||
Reference in New Issue
Block a user