Motor de escenarios de replicacion entre bases SAP Business One (Service Layer). Poll periodico sobre webhook_events, escenarios configurables via Prisma (filter + transform por escenario). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.1 KiB
Referencia de tablas
Todas viven en la base Postgres compartida (docker-compose.yml en la raíz
del workspace), definidas en prisma/schema.prisma.
Los ejemplos de esta página usan datos reales de la config actual del
ambiente de desarrollo (bases HQ01/SUB_01.."SUB_04", escenario
BP_PROVEEDORES).
webhook_events
Bandeja de entrada. sap-webhook-api inserta una fila acá por cada
notificación real que recibe de SAP (además de guardar el .json local).
No se toca a mano — el worker es el único que la actualiza.
| Columna | Tipo | Notas |
|---|---|---|
id |
String (PK) |
El id del CloudEvent que manda SAP. |
business_object |
String |
'BusinessPartners', 'SalesPersons', 'ProfitCenters', ... |
operation |
String |
'Created' | 'Updated'. |
subject |
String |
Clave del registro en la base que lo generó (CardCode, SalesEmployeeCode, CenterCode, según el objeto). |
payload |
Json |
El CloudEvent completo tal cual llegó — incluye source (/default/sap.b1/<CompanyDB>), que el motor usa para la protección contra loops (ver ARQUITECTURA.md). |
status |
EventStatus |
pending → processing → done | partial | failed | filtered. |
received_at |
DateTime |
databases
Registro único de toda base SAP alcanzable por Service Layer. Sin
distinción HQ/subsidiaria — ver ARQUITECTURA.md.
| Columna | Tipo | Notas |
|---|---|---|
id |
Int (PK, autoincremental) |
Identificador interno, no se usa como referencia legible. |
code |
String (único) |
Mnemónico corto y estable, asignado una sola vez al crear (ej. HQ01, SUB_01). No autoincremental — es lo que aparece en los logs. |
name |
String |
Descriptivo, solo para mostrar. |
base_url |
String |
URL del Service Layer, ej. https://sap10:50000/b1s/v2. |
company_db |
String |
CompanyDB de esa base. |
username / password |
String |
Credenciales de login del Service Layer. |
active |
Boolean |
Una base inactiva no se usa como origen ni destino en ningún escenario. |
Ejemplo (ambiente de desarrollo actual):
| code | name | base_url | company_db | active |
|---|---|---|---|---|
HQ01 |
HQ DemoCL | https://sap10:50000/b1s/v2 |
SBODemoCL |
true |
SUB_01 |
SUB 01 DemoCL | https://sap10:50000/b1s/v2 |
SUBDemoCL_01 |
true |
SUB_02 |
SUB 02 DemoCL | https://sap10:50000/b1s/v2 |
SUBDemoCL_02 |
true |
SUB_03 |
SUB 03 DemoCL | https://sap10:50000/b1s/v2 |
SUBDemoCL_03 |
true |
SUB_04 |
SUB 04 DemoCL | https://sap10:50000/b1s/v2 |
SUBDemoCL_04 |
true |
scenarios
Una fila por escenario. Ver ESCENARIOS.md para cómo
armar uno con el CLI.
| Columna | Tipo | Notas |
|---|---|---|
id |
Int (PK) |
|
code |
String (único) |
Identificador legible del escenario, en mayúsculas (ej. BP_PROVEEDORES). Determina el nombre de su archivo (filter + transform) en minúsculas: src/scenarios/<code en minúsculas>.ts (ej. bp_proveedores.ts) — ver scenarioFileSlug en src/scenarioTransform.ts. |
name |
String |
Descriptivo. |
business_object |
String |
Objeto que dispara el escenario: con qué se matchea webhook_events y con qué se lee el registro del origen. |
destination_business_object |
String? |
Objeto a usar en el GET/POST/PATCH del destino, si es distinto al de origen (ej. origen PurchaseOrders → destino Orders, típico de comercio intercompañía). null ⇒ mismo objeto que business_object (el caso común). |
origin_database_code |
String (FK → databases.code) |
De dónde se lee el registro completo. |
destination_mode |
ScenarioDestinationMode |
fixed (lista fija en scenario_destinations) | payload (destino resuelto por routing_field — ambos implementados). |
key_field |
String |
Campo clave del registro, ej. CardCode, SalesEmployeeCode. |
key_type |
ScenarioKeyType |
string (va entre comillas en la URL OData) | number (sin comillas — un campo numérico con comillas da HTTP 400). |
key_strategy |
ScenarioKeyStrategy |
portable (la clave vale lo mismo en origen y destino) | udfLink (SAP asigna la clave por base; se busca por match_field). |
match_field |
String? |
Solo si key_strategy = udfLink: nombre del UDF en el destino que guarda la clave de origen (ej. U_ITC_Source_JdtNum). |
routing_field |
String? |
Solo si destination_mode = payload: UDF en el origen cuyo valor es el code de la base destino (ej. U_ITC_Target). |
active |
Boolean |
No hay columnas de filtro: el filtro es la función filter() del archivo
del escenario (ver ARQUITECTURA.md).
Ejemplo (los 5 escenarios activos hoy en desarrollo):
| code | business_object | destination_business_object | origin | destinations | key_field | key_strategy |
|---|---|---|---|---|---|---|
BP_PROVEEDORES |
BusinessPartners |
— | HQ01 |
SUB_01, SUB_02 |
CardCode |
portable |
CHARTOFACCOUNTS |
ChartOfAccounts |
— | HQ01 |
SUB_01, SUB_02 |
Code |
portable |
JOURNALENTRIES |
JournalEntries |
— | HQ01 |
(payload: U_ITC_Target) |
JdtNum |
udfLink (U_ITC_Source_JdtNum) |
PROFITCENTERS |
ProfitCenters |
— | HQ01 |
SUB_01, SUB_02, SUB_03 |
CenterCode |
portable |
SALESPERSONS |
SalesPersons |
— | HQ01 |
SUB_01, SUB_03 |
SalesEmployeeCode |
udfLink (U_HQ_SlpCode) |
Ninguno usa todavía destination_business_object (ninguno lo necesita por
ahora) — quedaría, por ejemplo, PurchaseOrders → Orders en un futuro
escenario de compras intercompañía.
scenario_destinations
Destinos fijos de un escenario (solo aplica a destination_mode = fixed).
Único por (scenario_id, database_code).
| Columna | Tipo | Notas |
|---|---|---|
scenario_id |
Int (FK → scenarios.id) |
|
database_code |
String (FK → databases.code) |
scenario_value_mappings
Datos puros que el archivo de transformación de un escenario consulta vía
ctx.mapValue(fieldName, sourceValue) — ver
ARQUITECTURA.md. El código decide QUÉ campo mapear y
QUÉ hacer si falta el valor (tirar error, dejar sin traducir, etc.); acá
solo vive la equivalencia. Escopada por escenario (no global por
objeto+campo) porque el mismo campo puede tener dominios de valores
distintos según el escenario (ej. GroupCode de clientes vs. de
proveedores).
| Columna | Tipo | Notas |
|---|---|---|
scenario_id |
Int (FK) |
|
field_name |
String |
Ej. GroupCode. |
database_code |
String (FK → databases.code) |
El destino al que aplica esta traducción puntual. |
source_value |
String |
Valor tal como viene de la base origen. |
target_value |
String |
Valor a usar en ese destino. |
Único por (scenario_id, field_name, database_code, source_value).
Ejemplo real (BP_PROVEEDORES, GroupCode, hoy 1:1 en ambas subsidiarias):
| scenario | field_name | database_code | source_value | target_value |
|---|---|---|---|---|
BP_PROVEEDORES |
GroupCode |
SUB_01 |
101 |
101 |
BP_PROVEEDORES |
GroupCode |
SUB_01 |
110 |
110 |
BP_PROVEEDORES |
GroupCode |
SUB_02 |
101 |
101 |
| ... | (7 códigos × 2 destinos = 14 filas) |
replication_attempts
Log de auditoría inmutable. Una fila por cada intento de un escenario hacia una base destino.
| Columna | Tipo | Notas |
|---|---|---|
event_id |
String (FK → webhook_events.id) |
|
scenario_id |
Int (FK → scenarios.id) |
Imprescindible porque un evento puede pasar por varios escenarios a la vez. |
origin_database |
String |
Código de la base origen usada en este intento — texto libre, no FK: sobrevive si la base se borra o renombra después. |
destination_database |
String |
Ídem para el destino. 'N/A' si el intento falló antes de resolver un destino (ej. error leyendo el origen). |
verb_used |
String |
POST | PATCH | SKIPPED (falta mapeo, o transform tiró error) | N/A (error antes de llamar al destino). |
http_status |
Int? |
|
error_message |
String? |
|
payload_sent |
Json? |
El payload ya transformado enviado al destino — se guarda siempre, en éxito y en falla. |
result_key |
String? |
Para key_strategy=udfLink: la clave propia del destino (encontrada o recién asignada por SAP en un POST). null en key_strategy=portable y en datos maestros sin esta estrategia. |
attempted_at |
DateTime |
Índices sobre event_id, scenario_id y attempted_at, pensados para la
futura UI de consulta (sap-intercompany-viewer, ver ARQUITECTURA.md).
Ejemplo real (SALESPERSONS, vendedor nuevo SalesEmployeeCode=7 en HQ01,
POST en ambas subsidiarias — SAP le asignó el mismo número por
coincidencia, pero el motor no lo asume, lo captura del response):
| event_id | scenario | destination_database | verb_used | http_status | result_key |
|---|---|---|---|---|---|
149be29e-... |
SALESPERSONS |
SUB_01 |
POST |
201 |
7 |
149be29e-... |
SALESPERSONS |
SUB_03 |
POST |
201 |
7 |