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

@@ -115,6 +115,42 @@ def main():
print(f'{aplicados} items actualizados en Monday')
for code, motivo in omitidos:
print(f' omitido {code}: {motivo}')
if not DRY and aplicados:
notificar_alertas(cambios)
def notificar_alertas(cambios):
"""Avisa al API local de los cambios aplicados (alertas de inventario al
vendedor con cotizaciones recientes de la unidad). Best-effort: un fallo
aquí no revierte lo ya escrito en Monday."""
token = os.environ.get('INTERNAL_SYNC_TOKEN') or lee_env('INTERNAL_SYNC_TOKEN')
if not token:
print(' (sin INTERNAL_SYNC_TOKEN: se omiten las alertas de inventario)')
return
payload = json.dumps({'cambios': [
{'unidadCodigo': c['codigo'], 'statusNuevo': c['a']} for c in cambios
]}).encode()
req = urllib.request.Request(
'http://localhost:3000/api/cotizaciones/alertas-inventario',
data=payload,
headers={'Content-Type': 'application/json', 'X-Internal-Token': token})
try:
res = json.load(urllib.request.urlopen(req, timeout=15))
print(f" alertas-inventario: {res.get('alertasCreadas', 0)} actividad(es) creada(s), "
f"{res.get('omitidas', 0)} omitida(s), {res.get('fallidas', 0)} fallida(s)")
except Exception as e:
print(f' AVISO: no se pudieron enviar las alertas de inventario: {e}')
def lee_env(nombre):
env = os.path.join(ROOT, '../../.env')
try:
for line in open(env):
if line.startswith(nombre + '='):
return line.strip().split('=', 1)[1].strip().strip('"').strip("'")
except OSError:
pass
return None
if __name__ == '__main__':