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,28 @@
|
||||
// Listener HTTP mínimo, solo para esta prueba: simula sap-webhook-api pero
|
||||
// en vez de guardar en Postgres, escribe cada POST recibido como JSON en
|
||||
// tmp-test-events/ para poder inspeccionarlo. Se borra todo (este archivo
|
||||
// y la carpeta) al terminar la prueba.
|
||||
import { createServer } from 'node:http';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const outDir = fileURLToPath(new URL('../tmp-test-events/', import.meta.url));
|
||||
const port = 4100;
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => (body += chunk));
|
||||
req.on('end', () => {
|
||||
console.log(`[fake-webhook] ${req.method} ${req.url}`);
|
||||
if (req.method === 'POST') {
|
||||
const fileName = `${Date.now()}.json`;
|
||||
writeFileSync(`${outDir}${fileName}`, body);
|
||||
console.log(`[fake-webhook] guardado: tmp-test-events/${fileName}`);
|
||||
console.log(body);
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ received: true }));
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, () => console.log(`[fake-webhook] escuchando en http://127.0.0.1:${port}`));
|
||||
@@ -0,0 +1,29 @@
|
||||
// Exporta el body real de creación (Service Layer: UserTablesMD/UserFieldsMD)
|
||||
// de cada una de las 4 UserTables a un archivo JSON en json/user-tables/ —
|
||||
// útil para revisar el payload exacto, o para postearlo a mano (Postman,
|
||||
// curl, tools/sl.http) sin correr `npm run provision`.
|
||||
//
|
||||
// Se genera a partir de src/provisioning/table-definitions.ts (única fuente
|
||||
// de verdad) — si el esquema cambia ahí, correr de nuevo este script para
|
||||
// regenerar los JSON.
|
||||
//
|
||||
// Uso: npx tsx scripts/export-table-json.ts
|
||||
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { TABLE_DEFINITIONS } from '../src/provisioning/table-definitions.ts';
|
||||
import { toUserFieldPayload, toUserTablePayload } from '../src/provisioning/payloads.ts';
|
||||
|
||||
const outDir = fileURLToPath(new URL('../json/user-tables/', import.meta.url));
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
for (const definition of TABLE_DEFINITIONS) {
|
||||
const payload = {
|
||||
table: toUserTablePayload(definition),
|
||||
fields: definition.fields.map((field) => toUserFieldPayload(definition.tableName, field)),
|
||||
};
|
||||
|
||||
const filePath = `${outDir}${definition.tableName}.json`;
|
||||
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
||||
console.log(`Exportado: ${filePath}`);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// CLI de administración de suscripciones (@OEVC/@EVC1) contra el Service
|
||||
// Layer configurado en .env. Requiere que las 4 UserTables ya existan
|
||||
// (`npm run provision`).
|
||||
//
|
||||
// Uso:
|
||||
// npx tsx scripts/manage-subscriptions.ts subscription:add <webhookId> <webhookUrl> [--hmac-key <secreto>]
|
||||
// npx tsx scripts/manage-subscriptions.ts subscription:list
|
||||
// npx tsx scripts/manage-subscriptions.ts subscription:update-url <webhookId> <webhookUrl>
|
||||
// npx tsx scripts/manage-subscriptions.ts subscription:pause <webhookId>
|
||||
// npx tsx scripts/manage-subscriptions.ts subscription:resume <webhookId>
|
||||
//
|
||||
// npx tsx scripts/manage-subscriptions.ts filter:add <webhookId> <bizObject> <transType> <objectType>
|
||||
// npx tsx scripts/manage-subscriptions.ts filter:remove <webhookId> <bizObject> <transType>
|
||||
// npx tsx scripts/manage-subscriptions.ts filter:list
|
||||
|
||||
import { config } from '../src/config.ts';
|
||||
import { ServiceLayerClient } from '../src/service-layer-client.ts';
|
||||
import { generateCode } from '../src/lib/codes.ts';
|
||||
import { userTableEntity } from '../src/lib/service-layer-paths.ts';
|
||||
|
||||
const client = new ServiceLayerClient(config.serviceLayer);
|
||||
const OEVC = userTableEntity('OEVC');
|
||||
const EVC1 = userTableEntity('EVC1');
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(`Error: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function flag(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(`--${name}`);
|
||||
return index === -1 ? undefined : args[index + 1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
|
||||
switch (command) {
|
||||
case 'subscription:add': {
|
||||
const [webhookId, webhookUrl] = args;
|
||||
if (!webhookId || !webhookUrl) fail('subscription:add <webhookId> <webhookUrl> [--hmac-key <secreto>]');
|
||||
|
||||
const hmacKey = flag(args, 'hmac-key');
|
||||
await client.post(`/${OEVC}`, {
|
||||
Code: webhookId,
|
||||
Name: webhookId,
|
||||
U_WebhookURL: webhookUrl,
|
||||
U_AuthType: hmacKey ? 'H' : 'N',
|
||||
U_AuthCred: hmacKey ? JSON.stringify({ Key: hmacKey, Algorithm: 'hmacsha256' }) : null,
|
||||
U_State: 'A',
|
||||
U_WorkMode: 'S',
|
||||
});
|
||||
console.log(`Suscripción ${webhookId} creada (${webhookUrl}).`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscription:list': {
|
||||
const rows = await client.getAll(`/${OEVC}`);
|
||||
console.table(
|
||||
rows.map((row) => ({
|
||||
webhookId: row.Code,
|
||||
url: row.U_WebhookURL,
|
||||
authType: row.U_AuthType,
|
||||
state: row.U_State,
|
||||
})),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscription:update-url': {
|
||||
const [webhookId, webhookUrl] = args;
|
||||
if (!webhookId || !webhookUrl) fail('subscription:update-url <webhookId> <webhookUrl>');
|
||||
await client.patch(`/${OEVC}('${webhookId}')`, { U_WebhookURL: webhookUrl });
|
||||
console.log(`Suscripción ${webhookId} actualizada -> ${webhookUrl}.`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscription:pause': {
|
||||
const [webhookId] = args;
|
||||
if (!webhookId) fail('subscription:pause <webhookId>');
|
||||
await client.patch(`/${OEVC}('${webhookId}')`, { U_State: 'I' });
|
||||
console.log(`Suscripción ${webhookId} pausada (State='I').`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'subscription:resume': {
|
||||
const [webhookId] = args;
|
||||
if (!webhookId) fail('subscription:resume <webhookId>');
|
||||
await client.patch(`/${OEVC}('${webhookId}')`, { U_State: 'A' });
|
||||
console.log(`Suscripción ${webhookId} reactivada (State='A').`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter:add': {
|
||||
const [webhookId, bizObject, transType, objectType] = args;
|
||||
if (!webhookId || !bizObject || !transType || !objectType) {
|
||||
fail('filter:add <webhookId> <bizObject> <transType> <objectType>');
|
||||
}
|
||||
await client.post(`/${EVC1}`, {
|
||||
Code: generateCode(),
|
||||
Name: `${webhookId}/${bizObject}/${transType}`,
|
||||
U_WebhookID: webhookId,
|
||||
U_BizObject: bizObject,
|
||||
U_TransType: transType,
|
||||
U_ObjectType: objectType,
|
||||
});
|
||||
console.log(`Filtro agregado: ${webhookId} -> ${bizObject}/${transType} (ObjectType ${objectType}).`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter:remove': {
|
||||
const [webhookId, bizObject, transType] = args;
|
||||
if (!webhookId || !bizObject || !transType) {
|
||||
fail('filter:remove <webhookId> <bizObject> <transType>');
|
||||
}
|
||||
const filter = encodeURIComponent(
|
||||
`U_WebhookID eq '${webhookId}' and U_BizObject eq '${bizObject}' and U_TransType eq '${transType}'`,
|
||||
);
|
||||
const res = await client.get(`/${EVC1}?$filter=${filter}`);
|
||||
const rows = (res.data?.value as { Code: string }[]) ?? [];
|
||||
if (rows.length === 0) fail(`no existe el filtro ${webhookId} -> ${bizObject}/${transType}`);
|
||||
await client.delete(`/${EVC1}('${rows[0].Code}')`);
|
||||
console.log(`Filtro eliminado: ${webhookId} -> ${bizObject}/${transType}.`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'filter:list': {
|
||||
const rows = await client.getAll(`/${EVC1}`);
|
||||
console.table(
|
||||
rows.map((row) => ({
|
||||
webhookId: row.U_WebhookID,
|
||||
bizObject: row.U_BizObject,
|
||||
transType: row.U_TransType,
|
||||
objectType: row.U_ObjectType,
|
||||
})),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
fail(
|
||||
`comando desconocido: ${command ?? '(ninguno)'}\nComandos disponibles: subscription:add, subscription:list, ` +
|
||||
'subscription:update-url, subscription:pause, subscription:resume, filter:add, filter:remove, filter:list',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in new issue
Block a user