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

@@ -12,6 +12,7 @@ vi.mock('../config/index.js', () => ({
jwtExpiresIn: '7d',
adminPassword: 'Frasa2026',
},
internalSyncToken: 'test-internal-token',
},
}))
@@ -129,4 +130,42 @@ describe('authMiddleware (AUTH_ENABLED=true)', () => {
authMiddleware(reqFake({ method: 'GET', path: '/webhooks/monday' }), res, next)
expect(status).toHaveBeenCalledWith(401)
})
it('el token interno abre solo POST /cotizaciones/alertas-inventario', () => {
authMiddleware(
reqFake({
method: 'POST',
path: '/cotizaciones/alertas-inventario',
headers: { 'x-internal-token': 'test-internal-token' },
}),
resFake().res,
next,
)
expect(next).toHaveBeenCalledOnce()
})
it('token interno incorrecto o en otra ruta → 401', () => {
const casos: Partial<Request>[] = [
// token incorrecto en el endpoint correcto
{
method: 'POST',
path: '/cotizaciones/alertas-inventario',
headers: { 'x-internal-token': 'otro-token' },
},
// token correcto en otra ruta
{ method: 'POST', path: '/clientes', headers: { 'x-internal-token': 'test-internal-token' } },
{ method: 'GET', path: '/unidades', headers: { 'x-internal-token': 'test-internal-token' } },
// otro método sobre el endpoint
{
method: 'GET',
path: '/cotizaciones/alertas-inventario',
headers: { 'x-internal-token': 'test-internal-token' },
},
]
for (const c of casos) {
const { res, status } = resFake()
authMiddleware(reqFake(c), res, next)
expect(status).toHaveBeenCalledWith(401)
}
})
})