Files
sap-legacy-webhook-emulator/README.md
T

234 lines
12 KiB
Markdown

# 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`](../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`](./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
```bash
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`](../sap-test-legacy-webhook) (receptor mínimo
de prueba, HTTP o HTTPS):
```bash
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>`.
```bash
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](https://nssm.cc/) para que arranque solo con el
server y se reinicie si el proceso muere. Pasos (reemplazar
`<NombreServicio>` y las rutas por las reales):
```powershell
# 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`.
```powershell
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`](../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`](../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.