Initial commit: sap-intercompany-worker
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>
This commit is contained in:
commit
ca57403663
36 files changed
+5539
No files matched your search
@@ -0,0 +1,31 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EventStatus" AS ENUM ('pending', 'processing', 'done', 'partial', 'failed');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "webhook_events" (
|
||||
"id" TEXT NOT NULL,
|
||||
"business_object" TEXT NOT NULL,
|
||||
"operation" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"payload" JSONB NOT NULL,
|
||||
"received_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"status" "EventStatus" NOT NULL DEFAULT 'pending',
|
||||
|
||||
CONSTRAINT "webhook_events_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "replication_attempts" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"event_id" TEXT NOT NULL,
|
||||
"subsidiary" TEXT NOT NULL,
|
||||
"verb_used" TEXT NOT NULL,
|
||||
"http_status" INTEGER,
|
||||
"error_message" TEXT,
|
||||
"attempted_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "replication_attempts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "replication_attempts" ADD CONSTRAINT "replication_attempts_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "webhook_events"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,83 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ReplicationRuleType" AS ENUM ('master', 'transaction');
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "EventStatus" ADD VALUE 'filtered';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "replication_attempts" ADD COLUMN "result_key" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "subsidiaries" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"base_url" TEXT NOT NULL,
|
||||
"company_db" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"routing_value" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "subsidiaries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "replication_rules" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"business_object" TEXT NOT NULL,
|
||||
"type" "ReplicationRuleType" NOT NULL,
|
||||
"key_field" TEXT,
|
||||
"routing_field" TEXT,
|
||||
"idempotency_field" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "replication_rules_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "mapped_fields" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"business_object" TEXT NOT NULL,
|
||||
"field_name" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "mapped_fields_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "reference_field_mappings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"business_object" TEXT NOT NULL,
|
||||
"field_name" TEXT NOT NULL,
|
||||
"subsidiary_code" TEXT NOT NULL,
|
||||
"hq_value" TEXT NOT NULL,
|
||||
"subsidiary_value" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "reference_field_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subsidiaries_code_key" ON "subsidiaries"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "subsidiaries_routing_value_key" ON "subsidiaries"("routing_value");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "replication_rules_business_object_key" ON "replication_rules"("business_object");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "mapped_fields_business_object_field_name_key" ON "mapped_fields"("business_object", "field_name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "reference_field_mappings_business_object_field_name_subsidi_key" ON "reference_field_mappings"("business_object", "field_name", "subsidiary_code", "hq_value");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "reference_field_mappings" ADD CONSTRAINT "reference_field_mappings_subsidiary_code_fkey" FOREIGN KEY ("subsidiary_code") REFERENCES "subsidiaries"("code") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,144 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScenarioDestinationMode" AS ENUM ('fixed', 'payload');
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "reference_field_mappings" DROP CONSTRAINT "reference_field_mappings_subsidiary_code_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "replication_attempts" DROP COLUMN "subsidiary",
|
||||
ADD COLUMN "destination_database" TEXT NOT NULL,
|
||||
ADD COLUMN "origin_database" TEXT NOT NULL,
|
||||
ADD COLUMN "payload_sent" JSONB,
|
||||
ADD COLUMN "scenario_id" INTEGER NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "mapped_fields";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "reference_field_mappings";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "replication_rules";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "subsidiaries";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "ReplicationRuleType";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "databases" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"base_url" TEXT NOT NULL,
|
||||
"company_db" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "databases_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scenarios" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"business_object" TEXT NOT NULL,
|
||||
"origin_database_code" TEXT NOT NULL,
|
||||
"destination_mode" "ScenarioDestinationMode" NOT NULL,
|
||||
"key_field" TEXT NOT NULL,
|
||||
"routing_field" TEXT,
|
||||
"filter_field" TEXT,
|
||||
"filter_operator" TEXT,
|
||||
"filter_value" TEXT,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "scenarios_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scenario_destinations" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"scenario_id" INTEGER NOT NULL,
|
||||
"database_code" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "scenario_destinations_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scenario_mapped_fields" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"scenario_id" INTEGER NOT NULL,
|
||||
"field_name" TEXT NOT NULL,
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "scenario_mapped_fields_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "scenario_value_mappings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"scenario_id" INTEGER NOT NULL,
|
||||
"field_name" TEXT NOT NULL,
|
||||
"database_code" TEXT NOT NULL,
|
||||
"source_value" TEXT NOT NULL,
|
||||
"target_value" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "scenario_value_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "databases_code_key" ON "databases"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scenarios_code_key" ON "scenarios"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scenario_destinations_scenario_id_database_code_key" ON "scenario_destinations"("scenario_id", "database_code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scenario_mapped_fields_scenario_id_field_name_key" ON "scenario_mapped_fields"("scenario_id", "field_name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scenario_value_mappings_scenario_id_field_name_database_cod_key" ON "scenario_value_mappings"("scenario_id", "field_name", "database_code", "source_value");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "replication_attempts_event_id_idx" ON "replication_attempts"("event_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "replication_attempts_scenario_id_idx" ON "replication_attempts"("scenario_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "replication_attempts_attempted_at_idx" ON "replication_attempts"("attempted_at");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenarios" ADD CONSTRAINT "scenarios_origin_database_code_fkey" FOREIGN KEY ("origin_database_code") REFERENCES "databases"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_destinations" ADD CONSTRAINT "scenario_destinations_scenario_id_fkey" FOREIGN KEY ("scenario_id") REFERENCES "scenarios"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_destinations" ADD CONSTRAINT "scenario_destinations_database_code_fkey" FOREIGN KEY ("database_code") REFERENCES "databases"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_mapped_fields" ADD CONSTRAINT "scenario_mapped_fields_scenario_id_fkey" FOREIGN KEY ("scenario_id") REFERENCES "scenarios"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" ADD CONSTRAINT "scenario_value_mappings_scenario_id_fkey" FOREIGN KEY ("scenario_id") REFERENCES "scenarios"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" ADD CONSTRAINT "scenario_value_mappings_database_code_fkey" FOREIGN KEY ("database_code") REFERENCES "databases"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "replication_attempts" ADD CONSTRAINT "replication_attempts_scenario_id_fkey" FOREIGN KEY ("scenario_id") REFERENCES "scenarios"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "scenario_mapped_fields" DROP CONSTRAINT "scenario_mapped_fields_scenario_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" DROP CONSTRAINT "scenario_value_mappings_database_code_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" DROP CONSTRAINT "scenario_value_mappings_scenario_id_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "scenario_mapped_fields";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "scenario_value_mappings";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScenarioKeyStrategy" AS ENUM ('portable', 'udfLink');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "scenarios" ADD COLUMN "key_strategy" "ScenarioKeyStrategy" NOT NULL DEFAULT 'portable',
|
||||
ADD COLUMN "match_field" TEXT;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ScenarioKeyType" AS ENUM ('string', 'number');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "scenarios" ADD COLUMN "key_type" "ScenarioKeyType" NOT NULL DEFAULT 'string';
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "scenario_value_mappings" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"scenario_id" INTEGER NOT NULL,
|
||||
"field_name" TEXT NOT NULL,
|
||||
"database_code" TEXT NOT NULL,
|
||||
"source_value" TEXT NOT NULL,
|
||||
"target_value" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "scenario_value_mappings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "scenario_value_mappings_scenario_id_field_name_database_cod_key" ON "scenario_value_mappings"("scenario_id", "field_name", "database_code", "source_value");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" ADD CONSTRAINT "scenario_value_mappings_scenario_id_fkey" FOREIGN KEY ("scenario_id") REFERENCES "scenarios"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "scenario_value_mappings" ADD CONSTRAINT "scenario_value_mappings_database_code_fkey" FOREIGN KEY ("database_code") REFERENCES "databases"("code") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scenarios" DROP COLUMN "filter_field",
|
||||
DROP COLUMN "filter_operator",
|
||||
DROP COLUMN "filter_value";
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "scenarios" ADD COLUMN "destination_business_object" TEXT;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,177 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
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/<code>.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")
|
||||
}
|
||||
Reference in new issue
Block a user