# Guía: cómo armar un escenario Toda la configuración se administra con `scripts/manage-config.ts` (vía `tsx`), salvo el filtro y la transformación de cada escenario, que son código. Los cambios tienen efecto en el próximo tick del worker — no hace falta reiniciar el proceso, tampoco después de editar el archivo del escenario: el motor lo importa dinámicamente en cada intento. **Nombre del archivo**: el `code` de un escenario va en mayúsculas (es el identificador que ves en `scenario:list`, en los logs y en `--filter`/etc.) pero su archivo en `src/scenarios/` va en minúsculas — ej. código `BP_PROVEEDORES` → archivo `bp_proveedores.ts`. `scenario:add` ya genera el archivo con el nombre correcto; si necesitás renombrar un escenario después, usá `scenario:rename` (mueve el archivo automáticamente, no lo hagas a mano). ## 1. Registrar las bases de datos Antes de crear un escenario, las bases que va a usar (origen y destinos) tienen que existir en `databases`: ```bash npx tsx scripts/manage-config.ts database:add HQ01 "HQ DemoCL" https://sap10:50000/b1s/v2 SBODemoCL manager mngr npx tsx scripts/manage-config.ts database:add SUB_01 "SUB 01 DemoCL" https://sap10:50000/b1s/v2 SUBDemoCL_01 manager mngr npx tsx scripts/manage-config.ts database:add SUB_02 "SUB 02 DemoCL" https://sap10:50000/b1s/v2 SUBDemoCL_02 manager mngr npx tsx scripts/manage-config.ts database:list ``` **Importante**: si una base destino se creó clonando/restaurando la base origen (común en ambientes de demo), puede haber heredado su propia suscripción de webhook activa. Si el mismo `businessObject` que estás por replicar dispara un webhook en el destino, hay que desactivarlo (`EnableWebhook: tNO` en esa compañía) — si no, cada `PATCH`/`POST` nuestro genera una notificación que, aunque el motor la descarta (ver `ARQUITECTURA.md` § protección contra loops), sigue entrando como ruido. ## 2. Ejemplo 1: clave portable + mapeo de valor (`BP_PROVEEDORES`) Caso real: `BusinessPartners` con `CardType=cSupplier`, replicando solo algunos campos, con `GroupCode` traducido por destino. ### 2.1 Crear el escenario ```bash npx tsx scripts/manage-config.ts scenario:add BP_PROVEEDORES "BP - Proveedores" BusinessPartners HQ01 CardCode ``` `CardCode` es texto de negocio (igual en HQ01 y en cada subsidiaria), así que no hace falta `--key-strategy` ni `--key-type` — quedan en sus defaults (`portable` y `string`). Este comando genera automáticamente `src/scenarios/bp_proveedores.ts` (si no existía) con un `filter`/`transform` de partida (`filter` devuelve `true` siempre, `transform` devuelve el registro tal cual) — hay que editarlo con la condición real. ### 2.2 Definir los destinos ```bash npx tsx scripts/manage-config.ts scenario:add-destination BP_PROVEEDORES SUB_01 npx tsx scripts/manage-config.ts scenario:add-destination BP_PROVEEDORES SUB_02 ``` ### 2.3 Cargar los mapeos de valor (`GroupCode`) `GroupCode` en SAP significa "grupo de clientes" o "grupo de proveedores" según el `CardType` del registro — son dominios de valores distintos, aunque el nombre del campo sea el mismo. Por eso el mapeo se carga **por escenario**, no global: ```bash npx tsx scripts/manage-config.ts scenario:mapping:add BP_PROVEEDORES GroupCode SUB_01 101 101 npx tsx scripts/manage-config.ts scenario:mapping:add BP_PROVEEDORES GroupCode SUB_02 101 101 # ...un comando por cada (destino, valor de origen) — ver docs/TABLAS.md para la lista completa ``` ### 2.4 Escribir el filtro y la transformación `src/scenarios/bp_proveedores.ts` — el archivo siempre tiene las mismas dos partes. El filtro es la condición `CardType=cSupplier` (antes vivía como columnas en la tabla `scenarios`, ahora es código); la transformación selecciona solo los campos necesarios y resuelve `GroupCode` con `ctx.mapValue`, no con el valor crudo de HQ01: ```ts import type { ScenarioFilterContext, ScenarioTransformContext } from '../scenarioTransform.ts'; // --- Filtro: decide si este escenario aplica al registro --- // No necesita ctx acá: CardType ya viene en el payload de BusinessPartners. export function filter(record: Record, ctx: ScenarioFilterContext): boolean { return record.CardType === 'cSupplier'; } // --- Transformación: arma el payload para un destino puntual --- export function transform(record: Record, ctx: ScenarioTransformContext): Record { // --- Mapeo: GroupCode vive en Postgres (scenario_value_mappings), no acá --- const groupCode = record.GroupCode === undefined || record.GroupCode === null ? undefined : String(record.GroupCode); const mappedGroupCode = groupCode === undefined ? undefined : ctx.mapValue('GroupCode', groupCode); if (groupCode !== undefined && mappedGroupCode === undefined) { throw new Error(`GroupCode '${groupCode}' no tiene mapeo cargado para el destino '${ctx.destinationCode}'`); } return { CardCode: record.CardCode, CardName: record.CardName, CardType: record.CardType, GroupCode: mappedGroupCode, Phone1: record.Phone1, Phone2: record.Phone2, Notes: record.Notes, FederalTaxID: record.FederalTaxID, Currency: record.Currency, DebitorAccount: record.DebitorAccount, Valid: record.Valid, Frozen: record.Frozen, Properties1: record.Properties1, }; } ``` Si `filter` devuelve `false`, el escenario no aplica a ese registro (no genera ningún intento). Si `GroupCode` viene con un valor sin mapeo cargado dentro de `transform`, el `throw` marca el intento como `SKIPPED` (no manda nada sin traducir) y el motivo queda en `error_message` de `replication_attempts`. ## 3. Ejemplo 2: clave asignada por base (`SALESPERSONS`, `keyStrategy=udfLink`) `SalesPersons` usa `SalesEmployeeCode`, que SAP autonumera **por compañía** — el mismo vendedor tiene un código distinto en cada subsidiaria. No se puede usar `portable`. ### 3.1 Crear el UDF en SAP (paso manual, fuera de este repo) En cada subsidiaria destino, crear un campo de usuario en la tabla `OSLP` (Sales Employees) — ej. `U_HQ_SlpCode`, tipo **Alfanumérico** — desde Herramientas de Personalización / UDF Manager de SAP B1. Sin esto, el escenario no tiene dónde guardar la correspondencia. ### 3.2 Crear el escenario ```bash npx tsx scripts/manage-config.ts scenario:add SALESPERSONS "SalesPersons" SalesPersons HQ01 SalesEmployeeCode \ --key-strategy udfLink --match-field U_HQ_SlpCode --key-type number npx tsx scripts/manage-config.ts scenario:add-destination SALESPERSONS SUB_01 npx tsx scripts/manage-config.ts scenario:add-destination SALESPERSONS SUB_03 ``` `--key-type number` es necesario porque `SalesEmployeeCode` es numérico en el Service Layer — mandar la URL con comillas (`SalesPersons('5')`) da HTTP 400; sin comillas (`SalesPersons(5)`) funciona. ### 3.3 Cómo lo resuelve el motor (sin que tengas que hacer nada más) - Si ya existe un `SalesPersons` en el destino con `U_HQ_SlpCode` igual al `SalesEmployeeCode` de origen → lo encuentra, usa la clave propia del destino para el `PATCH`. - Si no existe ninguno → `POST` (con `U_HQ_SlpCode` seteado en el payload automáticamente por el motor — no hace falta ponerlo en el `transform`), y guarda la clave que SAP le asignó en `replication_attempts.result_key` para la próxima actualización. Sin lógica de negocio adicional pedida para este objeto, el `filter` (`return true`) y el `transform` (identity, `return { ...record }`) generados por `scenario:add` alcanzan tal cual. ## 4. Ejemplo 3: sin filtro, a 3 destinos (`PROFITCENTERS`) `ProfitCenters` usa `CenterCode`, un código de negocio asignado a mano (`CC1`, `Stelle_2`) — `portable`, igual que `BusinessPartners`: ```bash npx tsx scripts/manage-config.ts scenario:add PROFITCENTERS "ProfitCenters" ProfitCenters HQ01 CenterCode npx tsx scripts/manage-config.ts scenario:add-destination PROFITCENTERS SUB_01 npx tsx scripts/manage-config.ts scenario:add-destination PROFITCENTERS SUB_02 npx tsx scripts/manage-config.ts scenario:add-destination PROFITCENTERS SUB_03 ``` Sin condición pedida: `filter` queda como lo genera el scaffold (`return true`), aplica a todo `ProfitCenters`. ## 5. Filtro que necesita un dato que no viene en el payload Caso típico de transacciones: una `Invoices` (Factura de Venta) trae el `CardCode` del cliente, pero el escenario solo debe aplicar si ese cliente pertenece al `GroupCode` 101. El dato para decidir el filtro no está en el payload de la Factura — hay que ir a buscarlo a `BusinessPartners`. Por eso `filter` puede ser `async` y usar `ctx.get(...)` (mismo cliente/sesión que ya trae el registro principal, sin loguearse de nuevo): ```ts import type { ScenarioFilterContext, ScenarioTransformContext } from '../scenarioTransform.ts'; // --- Filtro: necesita el GroupCode del cliente, que no viene en la Factura --- export async function filter(record: Record, ctx: ScenarioFilterContext): Promise { const cardCode = record.CardCode; if (typeof cardCode !== 'string') return false; const res = await ctx.get(`/BusinessPartners('${cardCode}')`); if (res.status !== 200) { throw new Error(`No se pudo leer BusinessPartners('${cardCode}') de ${ctx.originCode}: HTTP ${res.status}`); } return res.data.GroupCode === 101; } ``` Si `ctx.get` tira o devuelve un status inesperado, el `throw` marca el intento como error (destino `'N/A'`, ver `ARQUITECTURA.md`) — no rompe la corrida completa, solo ese evento en ese escenario. **Importante**: esto resuelve la parte del filtro enriquecido, pero `Invoices` es una transacción (`destinationMode = 'payload'`), y ese modo **todavía no está implementado** en el motor (ver limitación en `ARQUITECTURA.md`) — hace falta además el ruteo del destino desde el propio mensaje antes de poder usar este escenario de punta a punta. ## 6. Comandos disponibles (referencia) ``` database:add database:disable database:list scenario:add [--key-strategy udfLink --match-field U_HQ_SlpCode] [--key-type number] [--destination-mode payload --routing-field U_ITC_Target] [--destination-object Orders] scenario:rename scenario:disable scenario:list scenario:add-destination scenario:remove-destination scenario:mapping:add scenario:mapping:list ``` `--destination-object` es para cuando el objeto cambia de nombre entre origen y destino — ej. un escenario que lee `PurchaseOrders` de HQ01 pero necesita crear un `Orders` (Orden de Venta) en la subsidiaria, típico de comercio intercompañía. Si se omite, el destino usa el mismo objeto que `` (el caso de los 5 escenarios de hoy). `scenario:add` y `database:add` son upsert por `code`: correr el mismo comando de nuevo actualiza la fila existente en vez de duplicarla. La primera vez que se crea un escenario nuevo, además genera su archivo en `src/scenarios/` con un `filter`/`transform` de partida — no lo sobrescribe en corridas siguientes. `scenario:rename` cambia el `code` en la DB **y** mueve el archivo correspondiente en un solo paso — no renombrar el archivo a mano, porque el motor lo busca por el `code` actual. `scenario:disable` / `database:disable` desactivan sin borrar — útil para pausar un escenario o sacar una base de servicio sin perder su configuración ni el historial de `replication_attempts` que la referencia.