Files
sap-webhook-api/docs/PASOS-REALIZADOS.md
T
GeorgeandClaude Sonnet 5 5e0df73477 Initial commit: sap-webhook-api
API NestJS que recibe el Webhook Messenger de SAP Business One
(CloudEvents), valida HMAC opcional y guarda cada notificacion
local (data/events/) e inserta en Postgres (webhook_events).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 07:41:25 -05:00

292 lines
9.6 KiB
Markdown

# Pasos realizados — API de Webhooks SAP Business One
Documento de los pasos ejecutados para levantar la API NestJS que escucha el
Event Notification del Webhook de SAP Business One Service Layer, desde el
proyecto vacío hasta tener la suscripción activa y lista para recibir
notificaciones reales. No incluye las pruebas contra SAP real (creación de
órdenes de venta) ni la investigación de errores posteriores — eso queda
documentado por separado.
## Contexto / objetivo
- API sencilla en NestJS que escucha el Webhook Messenger de SAP Business One
(Service Layer) y guarda localmente cada Event Notification recibido.
- SAP Business One corre en `sap10` (Service Layer, puerto `50000`) y en
`192.168.1.205` (Webhook Messenger, puerto `40008`), en la misma LAN que la
máquina de desarrollo (Mac, IP `192.168.1.166`).
- Compañía de pruebas: `SBODemoCL`, usuario `manager`.
## 1. Crear el proyecto NestJS
```bash
npx --yes @nestjs/cli new sap-webhook-api --package-manager npm --skip-git
```
Genera el scaffold estándar de Nest (TypeScript, npm) en `sap-webhook-api/`.
## 2. Crear el módulo de webhooks
```bash
cd sap-webhook-api
npx nest generate module webhooks --no-spec
npx nest generate controller webhooks --no-spec
```
Esto registra automáticamente `WebhooksModule` en `AppModule` y crea
`src/webhooks/webhooks.controller.ts`.
## 3. Endpoint mínimo que recibe el webhook
`src/webhooks/webhooks.controller.ts`:
```ts
import { Body, Controller, HttpCode, Logger, Post } from '@nestjs/common';
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
const EVENTS_DIR = join(process.cwd(), 'data', 'events');
interface SapEventNotification {
EventID?: string;
SourceDB?: string;
BusinessObject?: string;
Operation?: string;
Status?: string;
ReplayState?: string;
FieldNames?: string;
FieldValues?: string;
CreateDate?: string;
CreateTime?: string;
}
@Controller('webhooks')
export class WebhooksController {
private readonly logger = new Logger(WebhooksController.name);
@Post('sap')
@HttpCode(200)
receiveSapEvent(@Body() body: SapEventNotification | SapEventNotification[]) {
const notifications = Array.isArray(body) ? body : [body];
this.logger.log(`Recibidas ${notifications.length} notificación(es) de SAP`);
mkdirSync(EVENTS_DIR, { recursive: true });
notifications.forEach((notification, index) => {
const id = notification?.EventID ?? `${Date.now()}-${index}`;
const fileName = `${id}.json`;
writeFileSync(join(EVENTS_DIR, fileName), JSON.stringify(notification, null, 2));
this.logger.log(`Evento guardado: ${fileName}`);
});
return { received: true, count: notifications.length };
}
}
```
SAP puede enviar varias notificaciones "bateadas" en un solo `POST` (según
`MessageBatchLimit`), como confirma el sample oficial de SAP
(`webhook-service-sample-nodejs`). Por eso el body se normaliza siempre a un
array, y se guarda **un archivo por notificación**, nombrado por su
`EventID` cuando está disponible (o timestamp + índice como respaldo).
> Nota: esta versión reemplaza a una anterior más simple que guardaba el
> `body` completo en un solo archivo por request — ver historial de cambios
> del proyecto si se necesita esa versión.
## 4. Primera prueba local (HTTP)
```bash
npm run start
curl -s -i -X POST http://localhost:3000/webhooks/sap \
-H "Content-Type: application/json" -d '{"test":"hola"}'
```
Resultado: `200 OK`, `{"received":true}`, y log
`Evento recibido de SAP: {"test":"hola"}`.
## 5. Verificar conectividad de red en la LAN
- IP de la Mac en la LAN: `192.168.1.166` (`ipconfig getifaddr en0`).
- Confirmar que Node escucha en todas las interfaces, no solo localhost:
```bash
lsof -nP -iTCP:3000 -sTCP:LISTEN
# node ... TCP *:3000 (LISTEN)
```
- Desde el servidor SAP (Windows), verificar que puede llegar a la Mac en el
puerto `3000` (macOS no trae `telnet`, así que se usó `Test-NetConnection`):
```powershell
Test-NetConnection -ComputerName 192.168.1.166 -Port 3000
# TcpTestSucceeded : True
```
- Desde la Mac, verificar conectividad hacia el Webhook Messenger de SAP
(puerto `40008`) y hacia el Service Layer (puerto `50000`), con `nc`
(netcat, viene instalado en macOS):
```bash
nc -zv 192.168.1.205 40008 # Connection succeeded
nc -zv sap10 50000 # Connection succeeded
```
Con esto queda confirmada la conectividad en ambos sentidos.
## 6. Login en el Service Layer y estado del webhook
```bash
curl -sk -i -c cookies.txt -X POST https://sap10:50000/b1s/v2/Login \
-H "Content-Type: application/json" \
-d '{"CompanyDB":"SBODemoCL","UserName":"manager","Password":"mngr"}'
```
```bash
curl -sk -b cookies.txt https://sap10:50000/b1s/v2/CompanyService_GetAdminInfo
```
El campo `EnableWebhook` ya estaba en `"tYES"` en esta compañía, así que no
hizo falta llamar a `CompanyService_UpdateAdminInfo`.
## 7. Confirmar el evento en el catálogo
```bash
curl -sk -b cookies.txt \
"https://sap10:50000/b1s/v2/EventSubscriptionsService_GetEventCatalog"
```
Se confirmó la entrada:
```json
{"BusinessObject": "Orders", "ObjectType": "17", "TransactionTypes": "Created,Updated,Deleted,Closed,Cancelled,Reopened"}
```
## 8. Primer intento de suscripción (falló — HTTP no permitido)
```bash
curl -sk -i -b cookies.txt -X POST https://sap10:50000/b1s/v2/EventSubscriptions \
-H "Content-Type: application/json" -d '{
"WebhookID": "SapWebhookApiTest",
"WebhookURL": "http://192.168.1.166:3000/webhooks/sap",
"AuthenticationType": "None",
"Handshake": "tNO",
"EventCollection": [{"BusinessObject": "Orders", "TransactionType": "Created"}]
}'
```
Resultado: `400 Bad Request`, `-21100 Invalid webhook URL` — SAP exige que
`WebhookURL` sea HTTPS.
## 9. Certificado autofirmado y API en HTTPS
```bash
cd sap-webhook-api
mkdir -p certs
openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem \
-days 825 -nodes -subj "/CN=192.168.1.166" \
-addext "subjectAltName=IP:192.168.1.166"
```
`src/main.ts`:
```ts
import { readFileSync } from 'fs';
import { join } from 'path';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const httpsOptions = {
key: readFileSync(join(__dirname, '..', 'certs', 'key.pem')),
cert: readFileSync(join(__dirname, '..', 'certs', 'cert.pem')),
};
const app = await NestFactory.create(AppModule, { httpsOptions });
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
```
Prueba local tras el cambio:
```bash
curl -sk -i -X POST https://localhost:3000/webhooks/sap \
-H "Content-Type: application/json" -d '{"test":"https-ok"}'
ls -la data/events/
```
Resultado: `200 OK` y el archivo del evento guardado correctamente en
`data/events/`.
## 10. Crear la suscripción con HTTPS (éxito)
```bash
curl -sk -i -b cookies.txt -X POST https://sap10:50000/b1s/v2/EventSubscriptions \
-H "Content-Type: application/json" -d '{
"WebhookID": "SapWebhookApiTest",
"WebhookURL": "https://192.168.1.166:3000/webhooks/sap",
"AuthenticationType": "None",
"Handshake": "tNO",
"EventCollection": [{"BusinessObject": "Orders", "TransactionType": "Created"}]
}'
```
Resultado: `201 Created`, `State: "Active"` — pero con `VerifyCertificate:
"tYES"` (SAP intentaría validar nuestro certificado autofirmado y lo
rechazaría).
## 11. Desactivar la verificación de certificado
```bash
curl -sk -i -b cookies.txt -X PATCH \
"https://sap10:50000/b1s/v2/EventSubscriptions('SapWebhookApiTest')" \
-H "Content-Type: application/json" -d '{"VerifyCertificate": "tNO"}'
```
Resultado: `204 No Content`. Verificado con un `GET` posterior:
`VerifyCertificate: "tNO"`, `State: "Active"`.
## Estado al cierre de esta etapa
| Ítem | Estado |
|---|---|
| Proyecto NestJS (`sap-webhook-api`) | ✅ Creado |
| Endpoint `POST /webhooks/sap` | ✅ Responde 200 y guarda cada evento en `data/events/` |
| Conectividad LAN (Mac ↔ SAP, ambos sentidos) | ✅ Confirmada |
| Webhook habilitado en la compañía SAP | ✅ (ya estaba `tYES`) |
| Certificado HTTPS autofirmado | ✅ Generado y en uso |
| Suscripción `SapWebhookApiTest` (`Orders`/`Created`) | ✅ `Active`, `VerifyCertificate: tNO` |
A partir de aquí comienzan las pruebas con órdenes de venta reales en SAP, que
llevaron a identificar un bug en el Webhook Messenger de SAP (no cubierto en
este documento).
## Anexo: tablas internas de SAP para diagnóstico
Para diagnosticar problemas de entrega del webhook (suscripción activa pero
sin eventos llegando, errores de envío, etc.), conviene revisar directamente
estas tablas en la base de la compañía (ej. `SBODemoCL`), vía SSMS, en modo
**solo lectura**:
```sql
-- Suscripciones de webhook registradas (una fila por WebhookID: URL, estado,
-- fecha/hora de creación, último envío, último error)
SELECT * FROM OEVC
-- Cola de eventos de negocio generados (una fila por transacción que matchea
-- algún BusinessObject/TransactionType suscripto: EventID, CreateDate,
-- CreateTime, Status, BizObject, TransType). Status = 'N' significa
-- pendiente de enviar.
SELECT * FROM OEVN
-- Estado de envío de cada evento hacia cada webhook suscripto (relaciona
-- EventID + WebhookID con su resultado: Status, Retry, ReturnCode,
-- LastSentDT, LastSentTM)
SELECT * FROM EVN1
```
Estas tablas son internas del motor de SAP Business One — **no están
pensadas para escritura directa vía SQL** (cualquier cambio debería hacerse
a través de Service Layer / DI API). Sirven únicamente para inspeccionar el
estado real de la cola de eventos y de las suscripciones cuando el
comportamiento reportado por el Service Layer (`GET /EventSubscriptions`) no
alcanza para explicar por qué no llegan las notificaciones.