feat(api): automatizaciones Monday — round-robin de leads, recordatorio inactividad 10d, webhooks reales y polling cron
- Servicio de automatizaciones con estado persistido en SQLite (índice RR, dedup de recordatorios) - Webhook Monday procesa create_item del board Leads y asigna vendedor - Crons: polling cada 5 min (respaldo sin webhook público) + escaneo diario 9am - Endpoints admin: /api/automatizaciones/estado, /round-robin/run, /inactividad/preview - 15 tests nuevos (70/70 verdes)
This commit is contained in:
@@ -23,6 +23,27 @@ interface ItemMonday {
|
||||
column_values: ColumnValue[]
|
||||
}
|
||||
|
||||
interface ItemConFechas extends ItemMonday {
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/** Lead del tablero Leads tal como lo consumen las automatizaciones */
|
||||
export interface LeadMonday {
|
||||
id: string
|
||||
nombre: string
|
||||
updatedAt: string // ISO (updated_at del item en Monday)
|
||||
estado: string // label de la columna status 'Estado' ('' si no tiene)
|
||||
vendedorIds: number[] // user IDs asignados en la columna people 'Vendedor'
|
||||
}
|
||||
|
||||
/** Usuario del workspace (para la lista rotativa de vendedores) */
|
||||
export interface UsuarioMonday {
|
||||
id: number
|
||||
nombre: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface GraphQLErrorItem {
|
||||
message: string
|
||||
extensions?: { code?: string }
|
||||
@@ -350,6 +371,114 @@ export class MondayService {
|
||||
)
|
||||
this.invalidarCacheUnidades()
|
||||
}
|
||||
|
||||
// ---------- Leads (automatizaciones) ----------
|
||||
|
||||
/** Items del tablero Leads con estado, fecha de actualización y vendedor asignado */
|
||||
async getLeads(): Promise<LeadMonday[]> {
|
||||
const boardId = config.monday.boards.leads
|
||||
const cols = await this.columnasDe(boardId)
|
||||
const colVendedor = cols.get('Vendedor')
|
||||
if (!colVendedor || colVendedor.type !== 'people') {
|
||||
throw new AppError(500, `Columna people "Vendedor" no existe en el tablero Leads (${boardId})`)
|
||||
}
|
||||
const colEstado = cols.get('Estado')
|
||||
|
||||
const items: ItemConFechas[] = []
|
||||
let cursor: string | null = null
|
||||
do {
|
||||
const data: { boards: { items_page: { cursor: string | null; items: ItemConFechas[] } }[] } =
|
||||
cursor === null
|
||||
? await this.graphql(
|
||||
`query ($ids: [ID!]) {
|
||||
boards(ids: $ids) {
|
||||
items_page(limit: 200) {
|
||||
cursor
|
||||
items { id name created_at updated_at column_values { id text value } }
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ ids: [boardId] },
|
||||
)
|
||||
: await this.graphql(
|
||||
`query ($ids: [ID!], $cursor: String!) {
|
||||
boards(ids: $ids) {
|
||||
items_page(limit: 200, cursor: $cursor) {
|
||||
cursor
|
||||
items { id name created_at updated_at column_values { id text value } }
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ ids: [boardId], cursor },
|
||||
)
|
||||
const page = data.boards[0]?.items_page
|
||||
if (!page) break
|
||||
items.push(...page.items)
|
||||
cursor = page.cursor
|
||||
} while (cursor !== null)
|
||||
|
||||
return items.map((item) => {
|
||||
const porId = new Map(item.column_values.map((cv) => [cv.id, cv]))
|
||||
const rawVendedor = porId.get(colVendedor.id)?.value
|
||||
let vendedorIds: number[] = []
|
||||
if (rawVendedor) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawVendedor) as { personsAndTeams?: { id: number; kind: string }[] }
|
||||
vendedorIds = (parsed.personsAndTeams ?? []).filter((p) => p.kind === 'person').map((p) => p.id)
|
||||
} catch {
|
||||
vendedorIds = []
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: item.id,
|
||||
nombre: item.name,
|
||||
updatedAt: item.updated_at,
|
||||
estado: colEstado ? (porId.get(colEstado.id)?.text ?? '') : '',
|
||||
vendedorIds,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Asigna un vendedor (user ID de Monday) a la columna people "Vendedor" de un lead */
|
||||
async asignarVendedorALead(itemId: string, userId: number): Promise<void> {
|
||||
const boardId = config.monday.boards.leads
|
||||
const col = await this.columnaId(boardId, 'Vendedor')
|
||||
await this.graphql(
|
||||
`mutation ($boardId: ID!, $itemId: ID!, $columnId: String!, $value: JSON!) {
|
||||
change_column_value(board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) { id }
|
||||
}`,
|
||||
{
|
||||
boardId,
|
||||
itemId,
|
||||
columnId: col.id,
|
||||
value: JSON.stringify({ personsAndTeams: [{ id: userId, kind: 'person' }] }),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Crea un update (comentario) en un item — usado para los recordatorios de inactividad */
|
||||
async crearUpdateEnItem(itemId: string, body: string): Promise<string> {
|
||||
const data = await this.graphql<{ create_update: { id: string } }>(
|
||||
`mutation ($itemId: ID!, $body: String!) {
|
||||
create_update(item_id: $itemId, body: $body) { id }
|
||||
}`,
|
||||
{ itemId, body },
|
||||
)
|
||||
return data.create_update.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuarios activos no-guest del workspace (excluye las cuentas bot de Monday,
|
||||
* cuyo email termina en @agent.monday.com).
|
||||
*/
|
||||
async getUsuariosWorkspace(): Promise<UsuarioMonday[]> {
|
||||
const data = await this.graphql<{
|
||||
users: { id: string; name: string; email: string; is_guest: boolean; enabled: boolean }[]
|
||||
}>(`query { users(limit: 50) { id name email is_guest enabled } }`)
|
||||
return data.users
|
||||
.filter((u) => !u.is_guest && u.enabled && !u.email.endsWith('@agent.monday.com'))
|
||||
.map((u) => ({ id: Number(u.id), nombre: u.name, email: u.email }))
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- DTOs de escritura ----------
|
||||
|
||||
Reference in New Issue
Block a user