- POST /api/leads: captura ligera al tablero Leads (sin oportunidad), self-assign a Vendedor o round-robin; tambien cubre la captura manual desde la UI de Monday (webhook create_item re-registrado). - POST /api/afluencias (nuevo): la forma completa sigue cayendo en Oportunidades con todo el perfil (etapa Afluencia, columna Asesor). - Round-robin dual (ambos tableros), Mi dia mezcla leads+oportunidades con insignia de origen, acciones con parametro fuente, reportes: leads desde Leads, perfilamiento desde Oportunidades, funnel Leads->Afluencia->...->Ganado.
125 lines
5.5 KiB
TypeScript
125 lines
5.5 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'
|
|
import { GRUPOS_POR_ESTADO, moverAGrupoSegunEstado } from '../services/grupos-estado.service.js'
|
|
import { mondayService } from '../services/monday.service.js'
|
|
|
|
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()
|
|
|
|
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, 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=${idEvento ?? '?'}`)
|
|
|
|
// Monday registra el webhook como 'create_item' pero el payload real llega
|
|
// como 'create_pulse'; se aceptan ambos.
|
|
// Item nuevo en Leads u Oportunidades → solo round-robin del tablero
|
|
// correspondiente (los leads NO crean oportunidad; el Alta Afluencia entra
|
|
// directo a Oportunidades vía POST /api/afluencias). El grupo NO se arregla
|
|
// aquí: recién creado el item aún no tiene Estado (moverAGrupoSegunEstado no
|
|
// haría nada) y el create con column_values no dispara el webhook de change —
|
|
// para items creados a mano en la UI, el grupo lo corrige la próxima vez que
|
|
// cambie una columna.
|
|
if ((type === 'create_item' || type === 'create_pulse') && (esBoardLeads || esBoardOportunidades) && idEvento !== undefined) {
|
|
const id = String(idEvento)
|
|
const board = esBoardLeads ? 'leads' : 'oportunidades'
|
|
// Fire-and-forget: la respuesta 200 no espera a que termine la asignación
|
|
void automatizacionesService
|
|
.runRoundRobin(id, board)
|
|
.then((r) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[webhook monday] round-robin ${board} item=${id}: ${r.asignados} asignado(s)`)
|
|
})
|
|
.catch((err: unknown) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[webhook monday] error en round-robin ${board} item=${id}:`, err)
|
|
})
|
|
}
|
|
|
|
// 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.
|
|
const esEventoColumna =
|
|
type === 'change_status_column_value' || type === 'change_column_value' || type === 'update_column_value'
|
|
if (esEventoColumna && 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)
|
|
})
|
|
}
|
|
|
|
// Cambio de status en cualquier tablero con mapeo estado → grupo: el item se
|
|
// mueve al grupo que corresponde a su nuevo estado (regla de negocio: los
|
|
// grupos reflejan la etapa del pipeline). Mismo criterio que arriba: el
|
|
// payload solo dispara y el servicio re-consulta el item antes de mover.
|
|
// En Oportunidades convive con el escaneo (ambos disparan).
|
|
if (esEventoColumna && idEvento !== undefined && GRUPOS_POR_ESTADO[String(boardId)]) {
|
|
const board = String(boardId)
|
|
const id = String(idEvento)
|
|
void moverAGrupoSegunEstado(mondayService, board, id)
|
|
.then((r) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[webhook monday] grupo-estado board=${board} item=${id}: ${r.movido ? `movido a ${r.grupo}` : 'sin cambio'}`)
|
|
})
|
|
.catch((err: unknown) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[webhook monday] error en grupo-estado board=${board} item=${id}:`, err)
|
|
})
|
|
}
|
|
|
|
res.sendStatus(200)
|
|
}
|