# 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 `, `subscription:pause`/`subscription:resume`, `filter:list`, `filter:remove `. ```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 ` [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/.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.