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:
114
api-gateway/src/services/grupos-estado.service.test.ts
Normal file
114
api-gateway/src/services/grupos-estado.service.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('../config/index.js', () => ({
|
||||
config: {
|
||||
monday: {
|
||||
boards: {
|
||||
leads: '111',
|
||||
oportunidades: '222',
|
||||
contactos: '333',
|
||||
unidades: '444',
|
||||
apartado: '555',
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
import { GRUPOS_POR_ESTADO, grupoDestino, moverAGrupoSegunEstado } from './grupos-estado.service.js'
|
||||
|
||||
interface ItemFake {
|
||||
id: string
|
||||
name: string
|
||||
groupId: string
|
||||
column_values: { id: string; text: string | null; value: string | null }[]
|
||||
}
|
||||
|
||||
/** Monday mockeado: registra los movimientos de grupo solicitados */
|
||||
function mondayMock(opts: { item?: ItemFake | null; columnaId?: string | null } = {}) {
|
||||
return {
|
||||
movimientos: [] as { itemId: string; groupId: string }[],
|
||||
async columnaIdPorTitulo(_boardId: string, _titulo: string) {
|
||||
return opts.columnaId !== undefined ? opts.columnaId : 'status_col'
|
||||
},
|
||||
async getItemConGrupo(_itemId: string) {
|
||||
return opts.item ?? null
|
||||
},
|
||||
async moverItemAGrupo(itemId: string, groupId: string) {
|
||||
this.movimientos.push({ itemId, groupId })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const itemCon = (groupId: string, estado: string): ItemFake => ({
|
||||
id: '999',
|
||||
name: 'Item Prueba',
|
||||
groupId,
|
||||
column_values: [{ id: 'status_col', text: estado, value: null }],
|
||||
})
|
||||
|
||||
describe('grupoDestino', () => {
|
||||
it('mapea labels conocidos de cada tablero', () => {
|
||||
expect(grupoDestino('111', 'NUEVO')).toBe('topics')
|
||||
expect(grupoDestino('111', 'CONTACTADO')).toBe('group_mm6s1jct')
|
||||
expect(grupoDestino('111', 'DESCARTADO')).toBe('group_mm6s96mc')
|
||||
expect(grupoDestino('222', 'Ganado')).toBe('group_mm6scqz3')
|
||||
expect(grupoDestino('222', 'Afluencia')).toBe('topics')
|
||||
expect(grupoDestino('333', 'INACTIVO')).toBe('group_mm6sqqq2')
|
||||
expect(grupoDestino('444', 'Notaría')).toBe('group_mm6syf9c')
|
||||
expect(grupoDestino('555', 'LIBERADO')).toBe('group_mm6s2yyx')
|
||||
})
|
||||
|
||||
it('label desconocido → null', () => {
|
||||
expect(grupoDestino('111', 'INVENTADO')).toBeNull()
|
||||
})
|
||||
|
||||
it('board sin mapeo → null', () => {
|
||||
expect(grupoDestino('999', 'NUEVO')).toBeNull()
|
||||
})
|
||||
|
||||
it('etapa vacía en Unidades → null (no se mueve)', () => {
|
||||
expect(grupoDestino('444', '')).toBeNull()
|
||||
expect(grupoDestino('444', ' ')).toBeNull()
|
||||
})
|
||||
|
||||
it('los 5 tableros del CRM tienen mapeo', () => {
|
||||
expect(Object.keys(GRUPOS_POR_ESTADO).sort()).toEqual(['111', '222', '333', '444', '555'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('moverAGrupoSegunEstado', () => {
|
||||
it('mueve el item cuando el grupo actual difiere del destino', async () => {
|
||||
const monday = mondayMock({ item: itemCon('topics', 'DESCARTADO') })
|
||||
const r = await moverAGrupoSegunEstado(monday, '111', '999')
|
||||
expect(r).toEqual({ movido: true, grupo: 'group_mm6s96mc' })
|
||||
expect(monday.movimientos).toEqual([{ itemId: '999', groupId: 'group_mm6s96mc' }])
|
||||
})
|
||||
|
||||
it('no mueve cuando el item ya está en el grupo destino', async () => {
|
||||
const monday = mondayMock({ item: itemCon('group_mm6s96mc', 'DESCARTADO') })
|
||||
const r = await moverAGrupoSegunEstado(monday, '111', '999')
|
||||
expect(r).toEqual({ movido: false })
|
||||
expect(monday.movimientos).toEqual([])
|
||||
})
|
||||
|
||||
it('item inexistente (borrado tras el webhook) no truena ni mueve', async () => {
|
||||
const monday = mondayMock({ item: null })
|
||||
const r = await moverAGrupoSegunEstado(monday, '111', '999')
|
||||
expect(r).toEqual({ movido: false })
|
||||
expect(monday.movimientos).toEqual([])
|
||||
})
|
||||
|
||||
it('board sin mapeo no consulta ni mueve nada', async () => {
|
||||
const monday = mondayMock({ item: itemCon('topics', 'NUEVO') })
|
||||
const r = await moverAGrupoSegunEstado(monday, '999', '999')
|
||||
expect(r).toEqual({ movido: false })
|
||||
expect(monday.movimientos).toEqual([])
|
||||
})
|
||||
|
||||
it('label no mapeado no mueve', async () => {
|
||||
const monday = mondayMock({ item: itemCon('topics', 'INVENTADO') })
|
||||
const r = await moverAGrupoSegunEstado(monday, '111', '999')
|
||||
expect(r).toEqual({ movido: false })
|
||||
expect(monday.movimientos).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user