234 lines
12 KiB
Markdown
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.
|