refactor: las afluencias viven en Oportunidades (no en Leads)

- POST /api/leads crea UN solo item en Oportunidades (Estado Afluencia)
  con todo el perfil (desarrollo, modelo, estado civil, credito, etc.)
  en las columnas del board; dedup por telefono contra Oportunidades;
  asignacion al capturista via columna Asesor o round-robin del equipo.
- Round-robin, escalado e inactividad operan sobre Oportunidades (solo
  etapa Afluencia sin asesor entra al reparto).
- Acciones del vendedor (contactado/cita/descartar) escriben directo en
  la oportunidad; descarte = Perdido + Motivo No Compra.
- Mi dia y reportes leen Oportunidades; el funnel de /dash arranca en
  Afluencia; misOportunidades.porEtapa ya es real (columna Asesor).
- El picker del alta busca en Leads (historico) Y Oportunidades, con
  insignia de origen; moverAAparto tolera ambos ids.
- Lectura de status con value null = vacio (quirk del default Monday).
- Script de migracion del historial de agosto Leads -> Oportunidades.
This commit is contained in:
2026-09-04 04:46:34 +00:00
parent d4fc9ee892
commit 845f314560
26 changed files with 1496 additions and 985 deletions

View File

@@ -4,7 +4,6 @@ import type { Request, Response } from 'express'
const mocks = vi.hoisted(() => ({
runRoundRobin: vi.fn(),
escaneoOportunidades: vi.fn(),
asegurarAfluencia: vi.fn(),
moverAGrupo: vi.fn(),
}))
@@ -15,12 +14,6 @@ vi.mock('../services/automatizaciones.service.js', () => ({
},
}))
vi.mock('../services/funnel.service.js', () => ({
funnelService: {
asegurarOportunidadAfluenciaPorLeadId: mocks.asegurarAfluencia,
},
}))
vi.mock('../services/grupos-estado.service.js', () => ({
// Leads (111) y Oportunidades (222) con mapeo; el resto de boards, sin mapeo
GRUPOS_POR_ESTADO: {
@@ -52,7 +45,6 @@ beforeEach(() => {
vi.clearAllMocks()
mocks.runRoundRobin.mockResolvedValue({ asignados: 1 })
mocks.escaneoOportunidades.mockResolvedValue({ apartadosCreados: ['x'], ventasCreadas: [], errores: [] })
mocks.asegurarAfluencia.mockResolvedValue('op-1')
mocks.moverAGrupo.mockResolvedValue({ movido: true, grupo: 'topics' })
})
@@ -70,31 +62,34 @@ describe('webhookMonday', () => {
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.runRoundRobin).not.toHaveBeenCalled()
expect(mocks.escaneoOportunidades).not.toHaveBeenCalled()
expect(mocks.asegurarAfluencia).not.toHaveBeenCalled()
})
it('create_item en Leads dispara el round-robin del item', () => {
it('create_item en Oportunidades dispara el round-robin del item', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 111, itemId: 999 } }), res)
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 222, itemId: 999 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.runRoundRobin).toHaveBeenCalledWith('999')
expect(mocks.asegurarAfluencia).toHaveBeenCalledWith('999')
})
it('create_pulse (nombre real del evento) con pulseId también dispara el round-robin', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'create_pulse', boardId: 111, pulseId: 777 } }), res)
webhookMonday(reqCon({ event: { type: 'create_pulse', boardId: 222, pulseId: 777 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.runRoundRobin).toHaveBeenCalledWith('777')
expect(mocks.asegurarAfluencia).toHaveBeenCalledWith('777')
})
it('create_item en otro board no dispara round-robin ni afluencia', () => {
it('create_item en Leads ya NO dispara nada (tablero archivo histórico)', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 111, itemId: 999 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.runRoundRobin).not.toHaveBeenCalled()
})
it('create_item en otro board no dispara round-robin', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 555, itemId: 999 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.runRoundRobin).not.toHaveBeenCalled()
expect(mocks.asegurarAfluencia).not.toHaveBeenCalled()
})
it('change_status_column_value en Oportunidades dispara el escaneo', () => {
@@ -148,7 +143,7 @@ describe('webhookMonday', () => {
it('un error del servicio no rompe la respuesta 200 (fire-and-forget)', async () => {
mocks.runRoundRobin.mockRejectedValue(new Error('monday caído'))
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 111, itemId: 1 } }), res)
webhookMonday(reqCon({ event: { type: 'create_item', boardId: 222, itemId: 1 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
// Deja que el rechazo se procese para no ensuciar otros tests
await new Promise((r) => setImmediate(r))