feat(monday): mover items al grupo correcto al cambiar de estado

Webhook change_column_value en los 5 tableros (Leads, Oportunidades,
Unidades, Apartado, Contactos) + mapeo central estado->grupo: el API
re-consulta el item y lo mueve con move_item_to_group (idempotente).
El funnel mueve la oportunidad de Afluencia al crearla (Monday la crea
en el grupo top del board). Incluye script de backfill one-off
(249 items reubicados) y tests del mapeo y del webhook.
This commit is contained in:
2026-09-03 00:14:18 +00:00
parent 1ab29d765e
commit 89fcd1c579
8 changed files with 568 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ const mocks = vi.hoisted(() => ({
runRoundRobin: vi.fn(),
escaneoOportunidades: vi.fn(),
asegurarAfluencia: vi.fn(),
moverAGrupo: vi.fn(),
}))
vi.mock('../services/automatizaciones.service.js', () => ({
@@ -20,6 +21,15 @@ vi.mock('../services/funnel.service.js', () => ({
},
}))
vi.mock('../services/grupos-estado.service.js', () => ({
// Leads (111) y Oportunidades (222) con mapeo; el resto de boards, sin mapeo
GRUPOS_POR_ESTADO: {
'111': { columna: 'Estado', destinos: { NUEVO: 'topics' } },
'222': { columna: 'Estado', destinos: { Ganado: 'group_mm6scqz3' } },
},
moverAGrupoSegunEstado: mocks.moverAGrupo,
}))
vi.mock('../config/index.js', () => ({
config: {
monday: {
@@ -43,6 +53,7 @@ beforeEach(() => {
mocks.runRoundRobin.mockResolvedValue({ asignados: 1 })
mocks.escaneoOportunidades.mockResolvedValue({ apartadosCreados: ['x'], ventasCreadas: [], errores: [] })
mocks.asegurarAfluencia.mockResolvedValue('op-1')
mocks.moverAGrupo.mockResolvedValue({ movido: true, grupo: 'topics' })
})
describe('webhookMonday', () => {
@@ -113,6 +124,27 @@ describe('webhookMonday', () => {
expect(mocks.escaneoOportunidades).not.toHaveBeenCalled()
})
it('update_column_value en Leads dispara el mover a grupo según estado', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 111, itemId: 999 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.moverAGrupo).toHaveBeenCalledWith(expect.anything(), '111', '999')
})
it('update_column_value en Oportunidades dispara escaneo Y mover a grupo', () => {
const { res } = resFake()
webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 222, itemId: 5 } }), res)
expect(mocks.escaneoOportunidades).toHaveBeenCalledOnce()
expect(mocks.moverAGrupo).toHaveBeenCalledWith(expect.anything(), '222', '5')
})
it('update_column_value en un board sin mapeo no dispara el mover a grupo', () => {
const { res, sendStatus } = resFake()
webhookMonday(reqCon({ event: { type: 'update_column_value', boardId: 333, itemId: 1 } }), res)
expect(sendStatus).toHaveBeenCalledWith(200)
expect(mocks.moverAGrupo).not.toHaveBeenCalled()
})
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()