- Tablas ConfiguracionSistema (7 configs) + RolConfiguracion (6 roles) - Endpoints: config/sistema GET/PATCH, config/roles GET/PATCH, config/usuarios GET/PATCH, config/reset-roles POST, config/acciones GET - modulosDeRol() async con fallback a MATRIZ estatica - auth/me devuelve acciones[] segun rol del usuario - saludMonday.estaDisponible() lee modo_local_forzado de DB (cache 30s) - Landing: vista Configuraciones con 4 tabs (Modo, Roles, Usuarios, Sistema) - Mi-dia: boton Descartar se deshabilita si !descartar_lead - 630 API tests + 24 landing tests + 20 mi-dia tests verdes Seed: npm run seed:config
142 lines
6.4 KiB
TypeScript
142 lines
6.4 KiB
TypeScript
import cron from 'node-cron'
|
|
import { crearApp } from './app.js'
|
|
import { config } from './config/index.js'
|
|
import { automatizacionesService } from './services/automatizaciones.service.js'
|
|
import * as configService from './services/config.service.js'
|
|
import { mondayService } from './services/monday.service.js'
|
|
import { recordatoriosCobranzaService } from './services/recordatorios-cobranza.service.js'
|
|
import { saludMonday } from './services/salud-monday.service.js'
|
|
import { syncMondayService } from './services/sync-monday.service.js'
|
|
// Registra los handlers de cada tipo de SyncPendiente sobre el worker (efecto lateral)
|
|
import './services/sync-handlers.js'
|
|
|
|
const app = crearApp()
|
|
|
|
// Wiring del circuit breaker: el ping vive en monday.service; se inyecta aquí
|
|
// para que salud-monday.service no lo importe (ciclo de imports).
|
|
saludMonday.registrarPing(() => mondayService.ping())
|
|
|
|
// Sondeo de salud de Monday (cada 60 s): tras FALLOS_PARA_CAER fallos GraphQL
|
|
// seguidos el flag cae y la app opera en modo local; cuando el ping vuelve a
|
|
// responder se recupera el flag y se drena la cola de sincronización.
|
|
// Corre siempre (aunque MONDAY_AUTOMATIONS_ENABLED=false): es la infra del modo local.
|
|
cron.schedule('* * * * *', () => {
|
|
void (async () => {
|
|
if (process.env.MODO_LOCAL_FORZADO === 'true') return // modo local forzado: no sondear
|
|
if (saludMonday.estaDisponible()) return // Monday sano: nada que hacer
|
|
const recuperado = await saludMonday.sondear()
|
|
if (recuperado && saludMonday.estaDisponible()) {
|
|
// eslint-disable-next-line no-console
|
|
console.log('[salud] Monday recuperado; drenando cola de sincronización')
|
|
void syncMondayService.procesarPendientes() // fire-and-forget (mutex interno)
|
|
}
|
|
})().catch((err: unknown) => console.error('[cron salud] error en sondeo:', err))
|
|
})
|
|
|
|
// Sync worker del modo local (cada 2 min): drena SyncPendiente FIFO hacia
|
|
// Monday. No-op mientras Monday esté caído. Secuencial por diseño (SQLite).
|
|
cron.schedule('*/2 * * * *', () => {
|
|
syncMondayService
|
|
.procesarPendientes()
|
|
.catch((err: unknown) => console.error('[cron sync] error en la pasada:', err))
|
|
})
|
|
|
|
// 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, () => {
|
|
void (async () => {
|
|
// Modo local: Monday caído → saltar las reglas sin ruido (fallan seguro de todos modos)
|
|
if (!saludMonday.estaDisponible()) {
|
|
// eslint-disable-next-line no-console
|
|
console.debug('[cron poll] Monday no disponible (modo local); pasada omitida')
|
|
return
|
|
}
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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(
|
|
`[cron poll] oportunidades: ${r.apartadosCreados.length} apartado(s), ` +
|
|
`${r.ventasCreadas.length} venta(s), ${r.errores.length} error(es)`,
|
|
)
|
|
}
|
|
} catch (err: unknown) {
|
|
console.error('[cron poll] error en oportunidades:', err)
|
|
}
|
|
})()
|
|
})
|
|
|
|
// Escaneo diario de leads inactivos (recordatorio vía update en el item)
|
|
cron.schedule(config.automatizaciones.recordatorioCron, () => {
|
|
if (!saludMonday.estaDisponible()) {
|
|
// eslint-disable-next-line no-console
|
|
console.debug('[cron inactividad] Monday no disponible (modo local); escaneo omitido')
|
|
return
|
|
}
|
|
automatizacionesService
|
|
.escaneoInactividad({ preview: false })
|
|
.then((r) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[cron inactividad] ${r.recordatoriosEnviados} recordatorio(s) enviado(s)`)
|
|
})
|
|
.catch((err: unknown) => console.error('[cron inactividad] error en escaneo:', err))
|
|
})
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Automatizaciones Monday activas (poll: "${config.automatizaciones.pollCron}", inactividad: "${config.automatizaciones.recordatorioCron}")`)
|
|
|
|
// Escaneo diario de recordatorios de cobranza (pagos por vencer / vencidos)
|
|
cron.schedule(config.recordatoriosCobranza.cron, () => {
|
|
recordatoriosCobranzaService
|
|
.ejecutarPasada()
|
|
.then((r) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(
|
|
`[cron cobranza] recordatorios: ${r.enviados.length} enviado(s), ` +
|
|
`${r.evaluados.filter((e) => e.decision === 'dedup').length} deduplicado(s), ${r.fallidos.length} fallido(s)`,
|
|
)
|
|
})
|
|
.catch((err: unknown) => console.error('[cron cobranza] error en recordatorios:', err))
|
|
})
|
|
// eslint-disable-next-line no-console
|
|
console.log(`Recordatorios de cobranza activos (cron: "${config.recordatoriosCobranza.cron}")`)
|
|
}
|
|
|
|
// Precarga de configuraciones desde DB (modo_local_forzado, etc.)
|
|
void configService.refrescarCache().catch((err: unknown) =>
|
|
console.error('[startup] Error precargando config cache:', err),
|
|
)
|
|
|
|
app.listen(config.port, () => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`API Gateway FRASA escuchando en http://localhost:${config.port} (${config.nodeEnv})`)
|
|
})
|