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:
2026-08-28 16:46:08 +00:00
parent 30c682e4a0
commit 04941e9f80
10 changed files with 313 additions and 29 deletions

View File

@@ -6,36 +6,40 @@ import { recordatoriosCobranzaService } from './services/recordatorios-cobranza.
const app = crearApp()
// Crons de respaldo (el webhook público aún no existe: no hay SSL ni DNS).
// Se desactivan con MONDAY_AUTOMATIONS_ENABLED=false.
// Crons de respaldo: los webhooks de Monday (registrados sobre
// https://frasa.consultoria-as.com/api/webhooks/monday) disparan estas mismas
// reglas en tiempo real; estos barridos quedan como red de seguridad por si un
// evento se pierde. Se desactivan con MONDAY_AUTOMATIONS_ENABLED=false.
if (config.automatizaciones.enabled) {
// Polling: asigna vendedor (round-robin) a leads nuevos sin persona asignada,
// escala leads sin contactar y crea Apartados/Ventas al moverse Oportunidades.
// Cada regla tiene su propio catch: un fallo de Monday no rompe las demás.
// SECUENCIAL a propósito: SQLite serializa las escrituras y lanzar los tres
// escaneos en paralelo provoca timeouts P1008 en AutomatizacionState.
cron.schedule(config.automatizaciones.pollCron, () => {
automatizacionesService
.runRoundRobin()
.then((r) => {
void (async () => {
try {
const r = await automatizacionesService.runRoundRobin()
if (r.asignados > 0) {
// eslint-disable-next-line no-console
console.log(`[cron poll] round-robin: ${r.asignados} lead(s) asignado(s)`)
}
})
.catch((err: unknown) => console.error('[cron poll] error en round-robin:', err))
} catch (err: unknown) {
console.error('[cron poll] error en round-robin:', err)
}
automatizacionesService
.escaneoEscalado()
.then((r) => {
try {
const r = await automatizacionesService.escaneoEscalado()
if (r.escalados > 0) {
// eslint-disable-next-line no-console
console.log(`[cron poll] escalado: ${r.escalados} lead(s) escalado(s)`)
}
})
.catch((err: unknown) => console.error('[cron poll] error en escalado:', err))
} catch (err: unknown) {
console.error('[cron poll] error en escalado:', err)
}
automatizacionesService
.escaneoOportunidades()
.then((r) => {
try {
const r = await automatizacionesService.escaneoOportunidades()
if (r.apartadosCreados.length > 0 || r.ventasCreadas.length > 0 || r.errores.length > 0) {
// eslint-disable-next-line no-console
console.log(
@@ -43,8 +47,10 @@ if (config.automatizaciones.enabled) {
`${r.ventasCreadas.length} venta(s), ${r.errores.length} error(es)`,
)
}
})
.catch((err: unknown) => console.error('[cron poll] error en oportunidades:', err))
} catch (err: unknown) {
console.error('[cron poll] error en oportunidades:', err)
}
})()
})
// Escaneo diario de leads inactivos (recordatorio vía update en el item)