// Copia de sap-intercompany-worker/prisma/schema.prisma — mismo DATABASE_URL, // misma Postgres. Esta API solo corre `prisma generate` para tener un // cliente tipado propio; las migraciones (`prisma migrate`) corren SIEMPRE // desde sap-intercompany-worker, que es el dueño del schema. Si el schema // del worker cambia, copiar los cambios acá y volver a generar. generator client { provider = "prisma-client" output = "../src/generated/prisma" } datasource db { provider = "postgresql" } enum EventStatus { pending processing done partial failed filtered // ningún escenario activo para el objeto aceptó el mensaje: no se reintenta } enum ScenarioDestinationMode { fixed // datos maestros: la lista de destinos está fija en ScenarioDestination payload // transacciones: el destino se resuelve leyendo routingField del payload (no implementado aún, ver docs) } // Bandeja de entrada: un registro por evento recibido del webhook de SAP. // sap-webhook-api inserta acá (además del .json local) apenas recibe la notificación. model WebhookEvent { id String @id // el "id" del CloudEvent que manda SAP businessObject String @map("business_object") // 'BusinessPartners', 'Orders', etc. operation String // 'Created' | 'Updated' subject String // CardCode / DocEntry, según el objeto payload Json receivedAt DateTime @default(now()) @map("received_at") status EventStatus @default(pending) attempts ReplicationAttempt[] @@map("webhook_events") } // Registro único de toda base de datos SAP alcanzable por Service Layer. // Ya no existe distinción de HQ vs. subsidiaria: cualquier fila puede ser // origen o destino, según lo que declare cada escenario. model Database { id Int @id @default(autoincrement()) code String @unique // mnemónico corto y estable, ej. 'HQ', 'SUB-NORTE'; asignado una sola vez al crear, NO autoincremental name String baseUrl String @map("base_url") companyDb String @map("company_db") username String password String active Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") originOfScenarios Scenario[] @relation("ScenarioOrigin") destinations ScenarioDestination[] valueMappings ScenarioValueMapping[] @@map("databases") } enum ScenarioKeyStrategy { portable // el keyField vale lo mismo en origen y destino (ej. CardCode, ItemCode): GET directo por clave udfLink // SAP asigna la clave por base (ej. SalesEmployeeCode): se busca por un UDF en el destino que guarda la clave de origen } enum ScenarioKeyType { string // la clave va entre comillas en la URL OData: BusinessPartners('C23900') number // la clave va sin comillas: SalesPersons(5) — mandar comillas en un campo numérico da HTTP 400 } // Un escenario es una forma completa e independiente de procesar un business // object: su propio filtro, base origen, base(s) destino y transformación de // payload. Puede haber varios escenarios activos para el mismo business // object a la vez (ej. "clientes" y "proveedores" sobre BusinessPartners), y // un mismo evento puede matchear y ser procesado por más de uno. // // El filtro y la transformación del payload NO viven en esta tabla: cada // escenario tiene un archivo de código en src/scenarios/.ts con // SIEMPRE las mismas dos partes — filter(record) -> boolean y // transform(record, ctx) -> record — porque ambos necesitan lógica // arbitraria (condiciones compuestas, elegir campos) que una tabla no puede // expresar. Lo que SÍ vive en tabla son los mapeos de valor puntuales que // transform consulta vía ctx.mapValue(...) dentro de su lógica — ver // ScenarioValueMapping más abajo y docs/ESCENARIOS.md — pensado para que a // futuro una UI permita cargarlos sin editar código, aunque el archivo siga // siendo quien decide cómo usarlos. model Scenario { id Int @id @default(autoincrement()) code String @unique // 'BP_CLIENTES', 'BP_PROVEEDORES' — también el nombre del archivo en src/scenarios/ name String businessObject String @map("business_object") // objeto que dispara el escenario y que se lee del origen (con qué se matchea webhook_events y con qué se hace el GET a la base origen) destinationBusinessObject String? @map("destination_business_object") // objeto a usar en el GET/POST/PATCH del DESTINO, si es distinto al de origen (ej. origen PurchaseOrders -> destino Orders). null = mismo que businessObject originDatabaseCode String @map("origin_database_code") originDatabase Database @relation("ScenarioOrigin", fields: [originDatabaseCode], references: [code]) destinationMode ScenarioDestinationMode @map("destination_mode") keyField String @map("key_field") // clave del registro para GET/POST/PATCH, ej. 'CardCode', 'SalesEmployeeCode' keyType ScenarioKeyType @default(string) @map("key_type") // tipo de dato del keyField en el Service Layer (define si va entre comillas en la URL) keyStrategy ScenarioKeyStrategy @default(portable) @map("key_strategy") matchField String? @map("match_field") // solo si keyStrategy = udfLink: UDF en el destino que guarda el valor de keyField de origen, ej. 'U_HQ_SlpCode' routingField String? @map("routing_field") // solo si destinationMode = payload active Boolean @default(true) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") destinations ScenarioDestination[] valueMappings ScenarioValueMapping[] attempts ReplicationAttempt[] @@map("scenarios") } // Destinos fijos de un escenario en modo 'fixed' (datos maestros). En modo // 'payload' (transacciones) no se usa: el destino se resuelve en runtime. model ScenarioDestination { id Int @id @default(autoincrement()) scenarioId Int @map("scenario_id") scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade) databaseCode String @map("database_code") database Database @relation(fields: [databaseCode], references: [code]) createdAt DateTime @default(now()) @map("created_at") @@unique([scenarioId, databaseCode]) @@map("scenario_destinations") } // Tabla de equivalencias que el archivo de transformación del escenario // consulta vía ctx.mapValue(fieldName, sourceValue) — el código decide QUÉ // campo mapear y QUÉ hacer si falta (fallar, dejar sin traducir, etc.); acá // solo vive el dato. 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). Pensada para // que a futuro una UI la administre sin tocar código. model ScenarioValueMapping { id Int @id @default(autoincrement()) scenarioId Int @map("scenario_id") scenario Scenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade) fieldName String @map("field_name") databaseCode String @map("database_code") database Database @relation(fields: [databaseCode], references: [code]) sourceValue String @map("source_value") targetValue String @map("target_value") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([scenarioId, fieldName, databaseCode, sourceValue]) @@map("scenario_value_mappings") } // Un registro por cada intento de réplica de un evento, dentro de UN // escenario puntual, hacia UNA base destino. origin_database y // destination_database son texto libre (sin FK) a propósito: es un log de // auditoría inmutable que no debe verse afectado si la base o el escenario // se borran o renombran después. model ReplicationAttempt { id Int @id @default(autoincrement()) event WebhookEvent @relation(fields: [eventId], references: [id]) eventId String @map("event_id") scenario Scenario @relation(fields: [scenarioId], references: [id]) scenarioId Int @map("scenario_id") originDatabase String @map("origin_database") destinationDatabase String @map("destination_database") // 'N/A' si falló antes de resolver destino (ej. error leyendo el origen) verbUsed String @map("verb_used") // 'POST' | 'PATCH' | 'SKIPPED' | 'N/A' httpStatus Int? @map("http_status") errorMessage String? @map("error_message") payloadSent Json? @map("payload_sent") // payload ya transformado enviado al destino; se guarda en éxito Y en falla resultKey String? @map("result_key") // clave propia del destino cuando difiere de la de origen (keyStrategy=udfLink) o DocEntry (transacciones); null si no aplica attemptedAt DateTime @default(now()) @map("attempted_at") @@index([eventId]) @@index([scenarioId]) @@index([attemptedAt]) @@map("replication_attempts") }