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>
9.6 KiB
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, puerto50000) y en192.168.1.205(Webhook Messenger, puerto40008), en la misma LAN que la máquina de desarrollo (Mac, IP192.168.1.166). - Compañía de pruebas:
SBODemoCL, usuariomanager.
1. Crear el proyecto NestJS
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
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:
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
bodycompleto en un solo archivo por request — ver historial de cambios del proyecto si se necesita esa versión.
4. Primera prueba local (HTTP)
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:
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 traetelnet, así que se usóTest-NetConnection):
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 (puerto50000), connc(netcat, viene instalado en macOS):
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
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"}'
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
curl -sk -b cookies.txt \
"https://sap10:50000/b1s/v2/EventSubscriptionsService_GetEventCatalog"
Se confirmó la entrada:
{"BusinessObject": "Orders", "ObjectType": "17", "TransactionTypes": "Created,Updated,Deleted,Closed,Cancelled,Reopened"}
8. Primer intento de suscripción (falló — HTTP no permitido)
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
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:
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:
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)
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
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:
-- 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.