- 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)
65 lines
2.2 KiB
TypeScript
65 lines
2.2 KiB
TypeScript
import type { Request, Response } from 'express'
|
|
import { z } from 'zod'
|
|
import { config } from '../config/index.js'
|
|
import { automatizacionesService } from '../services/automatizaciones.service.js'
|
|
|
|
const eventoSchema = z
|
|
.object({
|
|
type: z.string(),
|
|
boardId: z.union([z.string(), z.number()]),
|
|
itemId: z.union([z.string(), z.number()]).optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
const payloadSchema = z.object({ event: eventoSchema }).passthrough()
|
|
|
|
/**
|
|
* POST /api/webhooks/monday
|
|
* Handshake: Monday envía { challenge } al registrar el webhook y exige que
|
|
* se responda tal cual. Eventos reales: se responde 200 de inmediato y se
|
|
* procesan en segundo plano.
|
|
*
|
|
* OJO: Monday NO firma sus webhooks → el payload no es confiable. Por eso el
|
|
* evento solo se usa como DISPARADOR: el round-robin re-consulta el tablero
|
|
* vía API antes de escribir nada.
|
|
*/
|
|
export function webhookMonday(req: Request, res: Response): void {
|
|
const body = req.body as Record<string, unknown>
|
|
|
|
if (typeof body?.['challenge'] === 'string') {
|
|
res.json({ challenge: body['challenge'] })
|
|
return
|
|
}
|
|
|
|
const parsed = payloadSchema.safeParse(body)
|
|
if (!parsed.success) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('[webhook monday] payload sin estructura de evento reconocida; se ignora')
|
|
res.sendStatus(200)
|
|
return
|
|
}
|
|
|
|
const { type, boardId, itemId } = parsed.data.event
|
|
const esBoardLeads = String(boardId) === config.monday.boards.leads
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[webhook monday] tipo=${type} board=${boardId} item=${itemId ?? '?'}`)
|
|
|
|
if (type === 'create_item' && esBoardLeads && itemId !== undefined) {
|
|
const id = String(itemId)
|
|
// Fire-and-forget: la respuesta 200 no espera a que termine la asignación
|
|
void automatizacionesService
|
|
.runRoundRobin(id)
|
|
.then((r) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[webhook monday] round-robin item=${id}: ${r.asignados} asignado(s)`)
|
|
})
|
|
.catch((err: unknown) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[webhook monday] error en round-robin item=${id}:`, err)
|
|
})
|
|
}
|
|
|
|
res.sendStatus(200)
|
|
}
|