sap-legacy-webhook-emulator

Reconstruye el mecanismo del Webhook Messenger nativo de SAP Business One (disponible desde FP2605) para clientes que todavía no están en esa versión, sin tocar nada de sap-webhook-api: termina mandando el mismo payload CloudEvents contra el mismo endpoint (POST /webhooks/sap) que mandaría el Messenger real.

Ver docs/DISENO.md para el diseño completo: por qué SBO_SP_PostTransactionNotice y no SBO_SP_TransactionNotification, por qué las 4 tablas son UserTablesMD en vez de SQL crudo, el diccionario de datos completo (esquema real de SAP vs. el que implementamos), los mapeos ObjectType↔BizObject / TransType↔Operation, y el diagrama de flujo del dispatcher.

Cómo funciona

  1. Captura: la lógica vive en SP_Webhook_CaptureEvent (sql/transaction-notification/sp-webhook-capture-event-mssql.sql) — hace un único INSERT en la UserTable @OEVN si el evento no es un eco de una escritura nuestra (ver docs/DISENO.md, hallazgo 5) y hay al menos una suscripción activa interesada en ese ObjectType+TransType (EXISTS contra @EVC1/@OEVC — lee la config real, no duplica ninguna lista). El hook compartido de SAP, SBO_SP_PostTransactionNotice (sql/transaction-notification/alter_sbo_sp_posttransactionnotice.sql), queda reducido a un solo EXEC hacia ese procedimiento — así nuestra lógica es identificable y no se mezcla con la de otros add-ons que puedan tener código en ese mismo hook. ⚠️ El filtro por suscripción activa está escrito pero no reinstalado ni reverificado contra un ambiente real todavía — ver docs/DISENO.md, "Pendientes" ítem 0.
  2. Provisioning: npm run provision crea (si no existen) las 4 UserTables — @OEVC/@EVC1/@OEVN/@EVN1 — vía Service Layer (UserTablesMD/UserFieldsMD), replicando el modelo de datos del Webhook Messenger real.
  3. Dispatcher (src/dispatcher/, corre con npm start): cada POLL_INTERVAL_SECONDS, un tick hace dos pasadas:
    • Registra eventos nuevos (@OEVN con Status='N'): resuelve BizObject/Operation (src/mapping/), los matchea contra las suscripciones activas en @OEVC+@EVC1, crea las filas de @EVN1 que falten, y descarta (borra) la fila de @OEVN si el ObjectType no mapea a nada conocido o si no hay ninguna suscripción interesada.
    • Despacha lo pendiente (@EVN1 con Status='F'): agrupa por suscripción, arma el batch de CloudEvents y hace un solo POST por WebhookURL (firmando con HMAC si la suscripción es AuthType='H'), actualiza @EVN1/@OEVN, e incrementa Retry en cada fallo. Al superar RETRY_LIMIT, la suscripción pasa a State='X' (Exceptional) y deja de reintentarse hasta que se reactive a mano.

Quick start

npm install
cp .env.example .env   # completar SL_* / POLL_INTERVAL_SECONDS / RETRY_LIMIT

npm run provision      # crea las 4 UserTables en la company DB del cliente

Si preferís revisar o postear el body exacto a mano (Postman, curl, tools/sl.http) en vez de correr npm run provision, están en json/user-tables/*.json — uno por tabla, con table (body de POST /UserTablesMD) y fields (bodies de POST /UserFieldsMD, en orden). Se regeneran con npx tsx scripts/export-table-json.ts si cambia table-definitions.ts.

Después, instalar en esa company DB, en este orden (SQL Server):

  1. sql/transaction-notification/sp-webhook-capture-event-mssql.sql (crea SP_Webhook_CaptureEvent).
  2. sql/transaction-notification/alter_sbo_sp_posttransactionnotice.sql (reemplaza el cuerpo de SBO_SP_PostTransactionNotice, que SAP crea vacío por cada compañía nueva, con el EXEC hacia el procedimiento anterior).

En HANA es el mismo orden, con sp-webhook-capture-event-hana.sql y recreate_sbo_sp_posttransactionnotice_hana.sql (que hace DROP+CREATE en vez de ALTER, porque HANA no soporta modificar el cuerpo de un procedimiento existente). ✅ Ambos ya se probaron e instalaron contra un SAP Business One sobre HANA real (captura y entrega confirmadas de punta a punta) — ⚠️ pero la versión actual del archivo (con el filtro por suscripción activa, ver más arriba) todavía no se reinstaló ni reverificó después de ese cambio.

Antes de arrancar el dispatcher, dar de alta al menos una suscripción (el destino y la autenticación salen de acá, no de .env). Para probar sin necesitar una instancia real de sap-webhook-api, usar sap-test-legacy-webhook (receptor mínimo de prueba, HTTP o HTTPS):

npx tsx scripts/manage-subscriptions.ts subscription:add SapWebhookApiTest https://192.168.1.166:3000/webhooks/sap
npx tsx scripts/manage-subscriptions.ts filter:add SapWebhookApiTest Orders A 17

Otros comandos disponibles: subscription:list, subscription:update-url <webhookId> <url>, subscription:pause/subscription:resume, filter:list, filter:remove <webhookId> <bizObject> <transType>.

npm run run-once   # una sola pasada, útil para probar
npm run start       # corre en loop

Correr como servicio de Windows (NSSM)

En Windows Server, el dispatcher (npm start) se deja instalado como servicio con NSSM para que arranque solo con el server y se reinicie si el proceso muere. Pasos (reemplazar <NombreServicio> y las rutas por las reales):

# 1. Instalar el servicio: Application = node.exe, Arguments = CLI de tsx + entrypoint
nssm install <NombreServicio> "C:\Program Files\nodejs\node.exe" "C:\ruta\al\proyecto\node_modules\tsx\dist\cli.mjs src/index.ts"

# 2. Directorio de trabajo -- IMPRESCINDIBLE: acá se resuelve el .env (dotenv lee del cwd)
nssm set <NombreServicio> AppDirectory "C:\ruta\al\proyecto"

# 3. Arranque automático con el server
nssm set <NombreServicio> Start SERVICE_AUTO_START

# 4. Reinicio automático si el proceso crashea
nssm set <NombreServicio> AppExit Default Restart

# 5. Captura de stderr como red de seguridad (la app ya loguea sola, ver
#    "Logs" abajo, así que Output/stdout no hace falta -- solo serviría
#    para duplicar lo mismo)
nssm set <NombreServicio> AppStderr "C:\ruta\al\proyecto\logs\nssm-stderr.log"

# 6. Arrancarlo
nssm start <NombreServicio>
Get-Service <NombreServicio>   # debe decir "Running"

nssm install <NombreServicio> sin más argumentos abre un instalador gráfico equivalente (útil si hay sesión de escritorio): en la pestaña Application van el Path/Startup directory/Arguments (pasos 1-2), en Details el Startup type (paso 3), en Exit actions el restart (paso 4) y en I/O el stderr (paso 5).

⚠️ No usar npm.cmd como Application — NSSM no trackea bien el proceso Node hijo que lanza npm, y al parar el servicio queda un proceso Node huérfano corriendo.

Para probar que el servicio instalado esté vivo (más allá de que Get-Service diga Running): seguir el log del día en vivo y confirmar que aparecen ticks nuevos cada POLL_INTERVAL_SECONDS.

Get-Content "C:\ruta\al\proyecto\logs\dispatcher-$(Get-Date -Format yyyy-MM-dd).log" -Tail 20 -Wait

Para desinstalar o reconfigurar: nssm stop <NombreServicio> seguido de nssm remove <NombreServicio> confirm, o nssm edit <NombreServicio> para el GUI de edición.

Logs

Cada tick escribe a logs/dispatcher-YYYY-MM-DD.log (uno por día, rotando a medianoche hora local del server) además de la consola — línea simple <timestamp ISO con offset local> [NIVEL] mensaje (ej. 2026-09-16T13:50:32.565-03:00 [INFO] ...), legible por cualquier visualizador genérico (tail -f logs/dispatcher-*.log, lnav, etc.). Se usa hora local (no UTC) a propósito para que los logs sean auditables directo contra la hora de pared, sin restar husos horarios a mano. Siempre deja al menos una línea de resumen por tick (Tick: N evento(s) nuevo(s), M pendiente(s) de entrega), incluso sin actividad — para poder confirmar que el proceso sigue vivo. No versionado (.gitignore).

Estructura

docs/DISENO.md               # diseño completo, diccionario de datos, decisiones, diagrama de flujo
sql/transaction-notification/
  sp-webhook-capture-event-mssql.sql               # SP_Webhook_CaptureEvent — la lógica real (SQL Server)
  alter_sbo_sp_posttransactionnotice.sql           # SBO_SP_PostTransactionNotice — un EXEC al de arriba
  sp-webhook-capture-event-hana.sql                # ídem, HANA/SQLScript — no verificado, ver aviso en el archivo
  recreate_sbo_sp_posttransactionnotice_hana.sql   # ídem, HANA (DROP+CREATE, no ALTER)
scripts/
  manage-subscriptions.ts     # CLI: alta/listado de suscripciones y filtros (@OEVC/@EVC1)
  export-table-json.ts        # regenera json/user-tables/*.json a partir de table-definitions.ts
json/user-tables/              # body real de UserTablesMD/UserFieldsMD por tabla, para revisar o postear a mano
  OEVC.json / EVC1.json / OEVN.json / EVN1.json
src/
  config.ts                  # env: Service Layer, poll interval, retry limit
  service-layer-client.ts    # cliente mínimo del Service Layer (Login/GET/POST/PATCH/DELETE)
  lib/
    codes.ts                  # Code sintético (UUID) para filas creadas desde Node
    timestamps.ts              # formato HHMMSS para U_LastSentTM
    cloud-event.ts              # arma el payload CloudEvents a partir de una fila de @OEVN
    logger.ts                   # log a consola + archivo (logs/dispatcher-YYYY-MM-DD.log)
  mapping/
    object-types.ts           # ObjectType -> BizObject
    trans-types.ts             # TransType -> Operation
  provisioning/
    table-definitions.ts      # esquema declarativo de las 4 UserTables (única fuente de verdad)
    payloads.ts                # traduce table-definitions.ts a los bodies reales de Service Layer
    user-tables.ts             # crea UserTablesMD/UserFieldsMD si no existen (idempotente)
    run-provision.ts           # CLI: npm run provision
  subscriptions/
    subscriptions-repository.ts  # acceso a @OEVC/@EVC1 (suscripciones activas y filtros)
  dispatcher/
    oevn-repository.ts         # acceso a @OEVN (leer/borrar/actualizar estado)
    evn1-repository.ts         # acceso a @EVN1 (crear, listar pendientes, marcar entrega/fallo)
    send-webhook.ts             # POST del batch de CloudEvents, firma HMAC opcional
    own-table-guard.ts          # detecta ecos de escrituras propias (ver docs/DISENO.md, hallazgo 5)
    process-tick.ts             # orquesta un tick completo
  index.ts                    # cron loop / --once

Stack

Mismo criterio que sap-intercompany-worker: script plano en TypeScript (ESM) con tsx, sin framework, sin base de datos propia — todo el estado vive en las 4 UserTables dentro de SAP.

Probar sin una instancia real de sap-webhook-api

sap-test-legacy-webhook es un receptor mínimo (sin framework, sin autenticación por ahora, HTTP o HTTPS con certificado autofirmado) pensado específicamente para probar este proyecto de punta a punta sin depender de sap-webhook-api. Guarda cada CloudEvent recibido en data/events/<id>.json. Usado para confirmar el flujo completo (captura → dispatcher → entrega) y, por separado, que HTTPS + certificado autofirmado funciona bien del lado del dispatcher.

Simplificaciones deliberadas de esta v1

  • Solo AuthType N (sin firma) y H (HMAC) — Basic y OAuth no están implementados.
  • Solo se maneja una clave simple en el evento (FieldsInKe=1, típicamente DocEntry) — claves compuestas (ej. CardCode+ItemCode) no se arman todavía en data del CloudEvent.
  • No hay CLI para administrar RETRY_LIMIT/State de forma más fina que pausar/reactivar toda la suscripción.
S
Description
No description provided
Readme
129 KiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 89.9%
TSQL 10.1%