Initial commit: sap-legacy-webhook-emulator
Reconstruye el Webhook Messenger nativo de SAP B1 (FP2605) para clientes que aun no estan en esa version: captura eventos via SBO_SP_PostTransactionNotice, provisiona UserTables OEVC/EVC1/OEVN/EVN1 y despacha CloudEvents al mismo endpoint que el messenger real. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
e1c8ee9015
42 files changed
+4042
No files matched your search
@@ -0,0 +1,178 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
Cada tick escribe a `logs/dispatcher-YYYY-MM-DD.log` (uno por día) además
|
||||
de la consola — línea simple `<timestamp ISO> [NIVEL] mensaje`, legible
|
||||
por cualquier visualizador genérico (`tail -f logs/dispatcher-*.log`,
|
||||
`lnav`, etc.). 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.
|
||||
Reference in new issue
Block a user