feat(api): webhooks Monday en tiempo real + alertas de inventario cableadas
- Webhook Oportunidades: cambio de etapa dispara escaneo (Aparto→Apartado, Ganado→Venta) además del round-robin de leads por create_item - Acepta los nombres reales de los eventos de Monday: create_pulse/pulseId y update_column_value (verificados en vivo) - Token interno (X-Internal-Token) para que los syncs locales llamen POST /cotizaciones/alertas-inventario sin JWT de usuario - sync-estatus-maestro.py notifica los cambios aplicados al endpoint de alertas (actividad al vendedor con cotizaciones recientes) - Fix P1008 SQLite: escaneos del poll cron secuenciales + connection_limit=1 en DATABASE_URL - Webhooks registrados en Monday sobre https://frasa.consultoria-as.com (Leads create_item, Oportunidades change_column_value); crons quedan como red de seguridad - Docs: sección 0 en AUTOMATIZACIONES-MONDAY.md con la matriz de eventos y los quirks de Monday
This commit is contained in:
@@ -7,7 +7,10 @@ const eventoSchema = z
|
||||
.object({
|
||||
type: z.string(),
|
||||
boardId: z.union([z.string(), z.number()]),
|
||||
// Monday manda pulseId en los eventos reales (create_pulse) y itemId en
|
||||
// las pruebas del panel de webhooks; se aceptan ambos.
|
||||
itemId: z.union([z.string(), z.number()]).optional(),
|
||||
pulseId: z.union([z.string(), z.number()]).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
@@ -39,14 +42,18 @@ export function webhookMonday(req: Request, res: Response): void {
|
||||
return
|
||||
}
|
||||
|
||||
const { type, boardId, itemId } = parsed.data.event
|
||||
const { type, boardId, itemId, pulseId } = parsed.data.event
|
||||
const esBoardLeads = String(boardId) === config.monday.boards.leads
|
||||
const esBoardOportunidades = String(boardId) === config.monday.boards.oportunidades
|
||||
const idEvento = itemId ?? pulseId
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`[webhook monday] tipo=${type} board=${boardId} item=${itemId ?? '?'}`)
|
||||
console.log(`[webhook monday] tipo=${type} board=${boardId} item=${idEvento ?? '?'}`)
|
||||
|
||||
if (type === 'create_item' && esBoardLeads && itemId !== undefined) {
|
||||
const id = String(itemId)
|
||||
// Monday registra el webhook como 'create_item' pero el payload real llega
|
||||
// como 'create_pulse'; se aceptan ambos.
|
||||
if ((type === 'create_item' || type === 'create_pulse') && esBoardLeads && idEvento !== undefined) {
|
||||
const id = String(idEvento)
|
||||
// Fire-and-forget: la respuesta 200 no espera a que termine la asignación
|
||||
void automatizacionesService
|
||||
.runRoundRobin(id)
|
||||
@@ -60,5 +67,32 @@ export function webhookMonday(req: Request, res: Response): void {
|
||||
})
|
||||
}
|
||||
|
||||
// Cambio de etapa en Oportunidades → crear Apartado/Venta si aplica.
|
||||
// El payload de Monday no es confiable (no va firmado), así que solo se usa
|
||||
// como disparador: el escaneo re-consulta el tablero y es idempotente (dedup
|
||||
// en AutomatizacionState). El cron de polling queda como red de seguridad.
|
||||
// OJO: el webhook se registra como 'change_column_value' pero el payload
|
||||
// real llega como 'update_column_value'; se aceptan los tres nombres.
|
||||
if (
|
||||
(type === 'change_status_column_value' ||
|
||||
type === 'change_column_value' ||
|
||||
type === 'update_column_value') &&
|
||||
esBoardOportunidades
|
||||
) {
|
||||
void automatizacionesService
|
||||
.escaneoOportunidades()
|
||||
.then((r) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`[webhook monday] oportunidades: ${r.apartadosCreados.length} apartado(s), ` +
|
||||
`${r.ventasCreadas.length} venta(s), ${r.errores.length} error(es)`,
|
||||
)
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('[webhook monday] error en escaneo de oportunidades:', err)
|
||||
})
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user