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 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) }