diff --git a/src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentImportBatchService.cs b/src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentImportBatchService.cs
new file mode 100644
index 0000000..d5bb7dd
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Admin/Services/EquipmentImportBatchService.cs
@@ -0,0 +1,207 @@
+using Microsoft.EntityFrameworkCore;
+using ZB.MOM.WW.OtOpcUa.Admin.Services;
+using ZB.MOM.WW.OtOpcUa.Configuration;
+using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+namespace ZB.MOM.WW.OtOpcUa.Admin.Services;
+
+///
+/// Staged-import orchestrator per Phase 6.4 Stream B.2-B.4. Covers the four operator
+/// actions: CreateBatch → StageRows (chunked) → FinaliseBatch (atomic apply into
+/// ) → DropBatch (rollback of pre-finalise state).
+///
+///
+/// FinaliseBatch runs inside one EF transaction + bulk-inserts accepted rows into
+/// . Rejected rows stay behind as audit evidence; the batch row
+/// gains so future writes know it's
+/// archived. DropBatch removes the batch + its cascaded rows.
+///
+/// Idempotence: calling FinaliseBatch twice throws
+/// rather than double-inserting. Operator refreshes the admin page to see the first
+/// finalise completed.
+///
+/// ExternalIdReservation merging (ZTag + SAPID uniqueness) is NOT done here — a
+/// narrower follow-up wires it once the concurrent-insert test matrix is green.
+///
+public sealed class EquipmentImportBatchService(OtOpcUaConfigDbContext db)
+{
+ /// Create a new empty batch header. Returns the row with Id populated.
+ public async Task CreateBatchAsync(string clusterId, string createdBy, CancellationToken ct)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
+ ArgumentException.ThrowIfNullOrWhiteSpace(createdBy);
+
+ var batch = new EquipmentImportBatch
+ {
+ Id = Guid.NewGuid(),
+ ClusterId = clusterId,
+ CreatedBy = createdBy,
+ CreatedAtUtc = DateTime.UtcNow,
+ };
+ db.EquipmentImportBatches.Add(batch);
+ await db.SaveChangesAsync(ct).ConfigureAwait(false);
+ return batch;
+ }
+
+ ///
+ /// Stage one chunk of rows into the batch. Caller usually feeds
+ /// output here — each
+ /// becomes one accepted ,
+ /// each rejected parser error becomes one row with false.
+ ///
+ public async Task StageRowsAsync(
+ Guid batchId,
+ IReadOnlyList acceptedRows,
+ IReadOnlyList rejectedRows,
+ CancellationToken ct)
+ {
+ var batch = await db.EquipmentImportBatches.FirstOrDefaultAsync(b => b.Id == batchId, ct).ConfigureAwait(false)
+ ?? throw new ImportBatchNotFoundException($"Batch {batchId} not found.");
+
+ if (batch.FinalisedAtUtc is not null)
+ throw new ImportBatchAlreadyFinalisedException(
+ $"Batch {batchId} finalised at {batch.FinalisedAtUtc:o}; no more rows can be staged.");
+
+ foreach (var row in acceptedRows)
+ {
+ db.EquipmentImportRows.Add(new EquipmentImportRow
+ {
+ Id = Guid.NewGuid(),
+ BatchId = batchId,
+ IsAccepted = true,
+ ZTag = row.ZTag,
+ MachineCode = row.MachineCode,
+ SAPID = row.SAPID,
+ EquipmentId = row.EquipmentId,
+ EquipmentUuid = row.EquipmentUuid,
+ Name = row.Name,
+ UnsAreaName = row.UnsAreaName,
+ UnsLineName = row.UnsLineName,
+ Manufacturer = row.Manufacturer,
+ Model = row.Model,
+ SerialNumber = row.SerialNumber,
+ HardwareRevision = row.HardwareRevision,
+ SoftwareRevision = row.SoftwareRevision,
+ YearOfConstruction = row.YearOfConstruction,
+ AssetLocation = row.AssetLocation,
+ ManufacturerUri = row.ManufacturerUri,
+ DeviceManualUri = row.DeviceManualUri,
+ });
+ }
+
+ foreach (var error in rejectedRows)
+ {
+ db.EquipmentImportRows.Add(new EquipmentImportRow
+ {
+ Id = Guid.NewGuid(),
+ BatchId = batchId,
+ IsAccepted = false,
+ RejectReason = error.Reason,
+ LineNumberInFile = error.LineNumber,
+ // Required columns need values for EF; reject rows use sentinel placeholders.
+ ZTag = "", MachineCode = "", SAPID = "", EquipmentId = "", EquipmentUuid = "",
+ Name = "", UnsAreaName = "", UnsLineName = "",
+ });
+ }
+
+ batch.RowsStaged += acceptedRows.Count + rejectedRows.Count;
+ batch.RowsAccepted += acceptedRows.Count;
+ batch.RowsRejected += rejectedRows.Count;
+
+ await db.SaveChangesAsync(ct).ConfigureAwait(false);
+ }
+
+ /// Drop the batch (pre-finalise rollback). Cascaded row delete removes staged rows.
+ public async Task DropBatchAsync(Guid batchId, CancellationToken ct)
+ {
+ var batch = await db.EquipmentImportBatches.FirstOrDefaultAsync(b => b.Id == batchId, ct).ConfigureAwait(false);
+ if (batch is null) return;
+ if (batch.FinalisedAtUtc is not null)
+ throw new ImportBatchAlreadyFinalisedException(
+ $"Batch {batchId} already finalised at {batch.FinalisedAtUtc:o}; cannot drop.");
+
+ db.EquipmentImportBatches.Remove(batch);
+ await db.SaveChangesAsync(ct).ConfigureAwait(false);
+ }
+
+ ///
+ /// Atomic finalise. Inserts every accepted row into the live
+ /// table under the target generation + stamps
+ /// . Failure rolls the whole tx
+ /// back — never partially mutates.
+ ///
+ public async Task FinaliseBatchAsync(
+ Guid batchId, long generationId, string driverInstanceIdForRows, string unsLineIdForRows, CancellationToken ct)
+ {
+ var batch = await db.EquipmentImportBatches
+ .Include(b => b.Rows)
+ .FirstOrDefaultAsync(b => b.Id == batchId, ct)
+ .ConfigureAwait(false)
+ ?? throw new ImportBatchNotFoundException($"Batch {batchId} not found.");
+
+ if (batch.FinalisedAtUtc is not null)
+ throw new ImportBatchAlreadyFinalisedException(
+ $"Batch {batchId} already finalised at {batch.FinalisedAtUtc:o}.");
+
+ // EF InMemory provider doesn't honour BeginTransaction; SQL Server provider does.
+ // Tests run the happy path under in-memory; production SQL Server runs the atomic tx.
+ var supportsTx = db.Database.IsRelational();
+ Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? tx = null;
+ if (supportsTx)
+ tx = await db.Database.BeginTransactionAsync(ct).ConfigureAwait(false);
+
+ try
+ {
+ foreach (var row in batch.Rows.Where(r => r.IsAccepted))
+ {
+ db.Equipment.Add(new Equipment
+ {
+ EquipmentRowId = Guid.NewGuid(),
+ GenerationId = generationId,
+ EquipmentId = row.EquipmentId,
+ EquipmentUuid = Guid.TryParse(row.EquipmentUuid, out var u) ? u : Guid.NewGuid(),
+ DriverInstanceId = driverInstanceIdForRows,
+ UnsLineId = unsLineIdForRows,
+ Name = row.Name,
+ MachineCode = row.MachineCode,
+ ZTag = row.ZTag,
+ SAPID = row.SAPID,
+ Manufacturer = row.Manufacturer,
+ Model = row.Model,
+ SerialNumber = row.SerialNumber,
+ HardwareRevision = row.HardwareRevision,
+ SoftwareRevision = row.SoftwareRevision,
+ YearOfConstruction = short.TryParse(row.YearOfConstruction, out var y) ? y : null,
+ AssetLocation = row.AssetLocation,
+ ManufacturerUri = row.ManufacturerUri,
+ DeviceManualUri = row.DeviceManualUri,
+ });
+ }
+
+ batch.FinalisedAtUtc = DateTime.UtcNow;
+ await db.SaveChangesAsync(ct).ConfigureAwait(false);
+ if (tx is not null) await tx.CommitAsync(ct).ConfigureAwait(false);
+ }
+ catch
+ {
+ if (tx is not null) await tx.RollbackAsync(ct).ConfigureAwait(false);
+ throw;
+ }
+ finally
+ {
+ if (tx is not null) await tx.DisposeAsync().ConfigureAwait(false);
+ }
+ }
+
+ /// List batches created by the given user. Finalised batches are archived; include them on demand.
+ public async Task> ListByUserAsync(string createdBy, bool includeFinalised, CancellationToken ct)
+ {
+ var query = db.EquipmentImportBatches.AsNoTracking().Where(b => b.CreatedBy == createdBy);
+ if (!includeFinalised)
+ query = query.Where(b => b.FinalisedAtUtc == null);
+ return await query.OrderByDescending(b => b.CreatedAtUtc).ToListAsync(ct).ConfigureAwait(false);
+ }
+}
+
+public sealed class ImportBatchNotFoundException(string message) : Exception(message);
+public sealed class ImportBatchAlreadyFinalisedException(string message) : Exception(message);
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs
new file mode 100644
index 0000000..c3b65f8
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs
@@ -0,0 +1,68 @@
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
+
+///
+/// Staged equipment-import batch per Phase 6.4 Stream B.2. Rows land in the child
+/// table under a batch header; operator reviews + either
+/// drops (via DropImportBatch) or finalises (via FinaliseImportBatch) in one
+/// bounded transaction. The live Equipment table never sees partial state.
+///
+///
+/// User-scoped visibility: the preview modal only shows batches where
+/// equals the current operator. Prevents accidental
+/// cross-operator finalise during concurrent imports. An admin finalise / drop surface
+/// can override this — tracked alongside the UI follow-up.
+///
+/// stamps the moment the batch promoted from staging
+/// into Equipment. Null = still in staging; non-null = archived / finalised.
+///
+public sealed class EquipmentImportBatch
+{
+ public Guid Id { get; set; }
+ public required string ClusterId { get; set; }
+ public required string CreatedBy { get; set; }
+ public DateTime CreatedAtUtc { get; set; }
+ public int RowsStaged { get; set; }
+ public int RowsAccepted { get; set; }
+ public int RowsRejected { get; set; }
+ public DateTime? FinalisedAtUtc { get; set; }
+
+ public ICollection Rows { get; set; } = [];
+}
+
+///
+/// One staged row under an . Mirrors the decision #117
+/// + decision #139 columns from the CSV importer's output + an
+/// flag + a string the preview modal
+/// renders.
+///
+public sealed class EquipmentImportRow
+{
+ public Guid Id { get; set; }
+ public Guid BatchId { get; set; }
+ public int LineNumberInFile { get; set; }
+ public bool IsAccepted { get; set; }
+ public string? RejectReason { get; set; }
+
+ // Required (decision #117)
+ public required string ZTag { get; set; }
+ public required string MachineCode { get; set; }
+ public required string SAPID { get; set; }
+ public required string EquipmentId { get; set; }
+ public required string EquipmentUuid { get; set; }
+ public required string Name { get; set; }
+ public required string UnsAreaName { get; set; }
+ public required string UnsLineName { get; set; }
+
+ // Optional (decision #139 — OPC 40010 Identification)
+ public string? Manufacturer { get; set; }
+ public string? Model { get; set; }
+ public string? SerialNumber { get; set; }
+ public string? HardwareRevision { get; set; }
+ public string? SoftwareRevision { get; set; }
+ public string? YearOfConstruction { get; set; }
+ public string? AssetLocation { get; set; }
+ public string? ManufacturerUri { get; set; }
+ public string? DeviceManualUri { get; set; }
+
+ public EquipmentImportBatch? Batch { get; set; }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.Designer.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.Designer.cs
new file mode 100644
index 0000000..a985f7d
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.Designer.cs
@@ -0,0 +1,1505 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using ZB.MOM.WW.OtOpcUa.Configuration;
+
+#nullable disable
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
+{
+ [DbContext(typeof(OtOpcUaConfigDbContext))]
+ [Migration("20260419185124_AddEquipmentImportBatch")]
+ partial class AddEquipmentImportBatch
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ApplicationUri")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("DashboardPort")
+ .HasColumnType("int");
+
+ b.Property("DriverConfigOverridesJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Host")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("nvarchar(255)");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("OpcUaPort")
+ .HasColumnType("int");
+
+ b.Property("RedundancyRole")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("ServiceLevelBase")
+ .HasColumnType("tinyint");
+
+ b.HasKey("NodeId");
+
+ b.HasIndex("ApplicationUri")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNode_ApplicationUri");
+
+ b.HasIndex("ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNode_Primary_Per_Cluster")
+ .HasFilter("[RedundancyRole] = 'Primary'");
+
+ b.ToTable("ClusterNode", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeCredential", b =>
+ {
+ b.Property("CredentialId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("NodeId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("RotatedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.HasKey("CredentialId");
+
+ b.HasIndex("Kind", "Value")
+ .IsUnique()
+ .HasDatabaseName("UX_ClusterNodeCredential_Value")
+ .HasFilter("[Enabled] = 1");
+
+ b.HasIndex("NodeId", "Enabled")
+ .HasDatabaseName("IX_ClusterNodeCredential_NodeId");
+
+ b.ToTable("ClusterNodeCredential", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CurrentGenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("LastAppliedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastAppliedError")
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("LastAppliedStatus")
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("NodeId");
+
+ b.HasIndex("CurrentGenerationId")
+ .HasDatabaseName("IX_ClusterNodeGenerationState_Generation");
+
+ b.ToTable("ClusterNodeGenerationState", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigAuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AuditId"));
+
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DetailsJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EventType")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Principal")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Timestamp")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("GenerationId")
+ .HasDatabaseName("IX_ConfigAuditLog_Generation")
+ .HasFilter("[GenerationId] IS NOT NULL");
+
+ b.HasIndex("ClusterId", "Timestamp")
+ .IsDescending(false, true)
+ .HasDatabaseName("IX_ConfigAuditLog_Cluster_Time");
+
+ b.ToTable("ConfigAuditLog", null, t =>
+ {
+ t.HasCheckConstraint("CK_ConfigAuditLog_DetailsJson_IsJson", "DetailsJson IS NULL OR ISJSON(DetailsJson) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", b =>
+ {
+ b.Property("GenerationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("GenerationId"));
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("ParentGenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("PublishedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("PublishedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.HasKey("GenerationId");
+
+ b.HasIndex("ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_ConfigGeneration_Draft_Per_Cluster")
+ .HasFilter("[Status] = 'Draft'");
+
+ b.HasIndex("ParentGenerationId");
+
+ b.HasIndex("ClusterId", "Status", "GenerationId")
+ .IsDescending(false, false, true)
+ .HasDatabaseName("IX_ConfigGeneration_Cluster_Published");
+
+ SqlServerIndexBuilderExtensions.IncludeProperties(b.HasIndex("ClusterId", "Status", "GenerationId"), new[] { "PublishedAt" });
+
+ b.ToTable("ConfigGeneration", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Device", b =>
+ {
+ b.Property("DeviceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DeviceConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.HasKey("DeviceRowId");
+
+ b.HasIndex("GenerationId", "DeviceId")
+ .IsUnique()
+ .HasDatabaseName("UX_Device_Generation_LogicalId")
+ .HasFilter("[DeviceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_Device_Generation_Driver");
+
+ b.ToTable("Device", null, t =>
+ {
+ t.HasCheckConstraint("CK_Device_DeviceConfig_IsJson", "ISJSON(DeviceConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverHostStatus", b =>
+ {
+ b.Property("NodeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HostName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Detail")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("LastSeenUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("State")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("StateChangedUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("NodeId", "DriverInstanceId", "HostName");
+
+ b.HasIndex("LastSeenUtc")
+ .HasDatabaseName("IX_DriverHostStatus_LastSeen");
+
+ b.HasIndex("NodeId")
+ .HasDatabaseName("IX_DriverHostStatus_Node");
+
+ b.ToTable("DriverHostStatus", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance", b =>
+ {
+ b.Property("DriverInstanceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("NamespaceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ResilienceConfig")
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("DriverInstanceRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_DriverInstance_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .IsUnique()
+ .HasDatabaseName("UX_DriverInstance_Generation_LogicalId")
+ .HasFilter("[DriverInstanceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "NamespaceId")
+ .HasDatabaseName("IX_DriverInstance_Generation_Namespace");
+
+ b.ToTable("DriverInstance", null, t =>
+ {
+ t.HasCheckConstraint("CK_DriverInstance_DriverConfig_IsJson", "ISJSON(DriverConfig) = 1");
+
+ t.HasCheckConstraint("CK_DriverInstance_ResilienceConfig_IsJson", "ResilienceConfig IS NULL OR ISJSON(ResilienceConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstanceResilienceStatus", b =>
+ {
+ b.Property("DriverInstanceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HostName")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("BaselineFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("ConsecutiveFailures")
+ .HasColumnType("int");
+
+ b.Property("CurrentBulkheadDepth")
+ .HasColumnType("int");
+
+ b.Property("CurrentFootprintBytes")
+ .HasColumnType("bigint");
+
+ b.Property("LastCircuitBreakerOpenUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastRecycleUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("LastSampledUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.HasKey("DriverInstanceId", "HostName");
+
+ b.HasIndex("LastSampledUtc")
+ .HasDatabaseName("IX_DriverResilience_LastSampled");
+
+ b.ToTable("DriverInstanceResilienceStatus", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
+ {
+ b.Property("EquipmentRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("AssetLocation")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DeviceManualUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("EquipmentClassRef")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("EquipmentId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentUuid")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("HardwareRevision")
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("MachineCode")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Manufacturer")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ManufacturerUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Model")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("SAPID")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SerialNumber")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("SoftwareRevision")
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("UnsLineId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("YearOfConstruction")
+ .HasColumnType("smallint");
+
+ b.Property("ZTag")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("EquipmentRowId");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_Equipment_Generation_Driver");
+
+ b.HasIndex("GenerationId", "EquipmentId")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_LogicalId")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "EquipmentUuid")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_Uuid");
+
+ b.HasIndex("GenerationId", "MachineCode")
+ .HasDatabaseName("IX_Equipment_Generation_MachineCode");
+
+ b.HasIndex("GenerationId", "SAPID")
+ .HasDatabaseName("IX_Equipment_Generation_SAPID")
+ .HasFilter("[SAPID] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsLineId")
+ .HasDatabaseName("IX_Equipment_Generation_Line");
+
+ b.HasIndex("GenerationId", "ZTag")
+ .HasDatabaseName("IX_Equipment_Generation_ZTag")
+ .HasFilter("[ZTag] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsLineId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Equipment_Generation_LinePath");
+
+ b.ToTable("Equipment", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("FinalisedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("RowsAccepted")
+ .HasColumnType("int");
+
+ b.Property("RowsRejected")
+ .HasColumnType("int");
+
+ b.Property("RowsStaged")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy", "FinalisedAtUtc")
+ .HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
+
+ b.ToTable("EquipmentImportBatch", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AssetLocation")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("BatchId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DeviceManualUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("EquipmentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentUuid")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("HardwareRevision")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("IsAccepted")
+ .HasColumnType("bit");
+
+ b.Property("LineNumberInFile")
+ .HasColumnType("int");
+
+ b.Property("MachineCode")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Manufacturer")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ManufacturerUri")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Model")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("RejectReason")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("SAPID")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("SerialNumber")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("SoftwareRevision")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsAreaName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsLineName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("YearOfConstruction")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("ZTag")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BatchId")
+ .HasDatabaseName("IX_EquipmentImportRow_Batch");
+
+ b.ToTable("EquipmentImportRow", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ExternalIdReservation", b =>
+ {
+ b.Property("ReservationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentUuid")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("FirstPublishedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("FirstPublishedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("LastPublishedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("ReleaseReason")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("ReleasedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ReleasedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("ReservationId");
+
+ b.HasIndex("EquipmentUuid")
+ .HasDatabaseName("IX_ExternalIdReservation_Equipment");
+
+ b.HasIndex("Kind", "Value")
+ .IsUnique()
+ .HasDatabaseName("UX_ExternalIdReservation_KindValue_Active")
+ .HasFilter("[ReleasedAt] IS NULL");
+
+ b.ToTable("ExternalIdReservation", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.LdapGroupRoleMapping", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("IsSystemWide")
+ .HasColumnType("bit");
+
+ b.Property("LdapGroup")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("Role")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("LdapGroup")
+ .HasDatabaseName("IX_LdapGroupRoleMapping_Group");
+
+ b.HasIndex("LdapGroup", "ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_LdapGroupRoleMapping_Group_Cluster")
+ .HasFilter("[ClusterId] IS NOT NULL");
+
+ b.ToTable("LdapGroupRoleMapping", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
+ {
+ b.Property("NamespaceRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Kind")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("NamespaceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("NamespaceUri")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.HasKey("NamespaceRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_Namespace_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "NamespaceId")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_LogicalId")
+ .HasFilter("[NamespaceId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "NamespaceUri")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_NamespaceUri");
+
+ b.HasIndex("GenerationId", "ClusterId", "Kind")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_Cluster_Kind");
+
+ b.HasIndex("GenerationId", "NamespaceId", "ClusterId")
+ .IsUnique()
+ .HasDatabaseName("UX_Namespace_Generation_LogicalId_Cluster")
+ .HasFilter("[NamespaceId] IS NOT NULL");
+
+ b.ToTable("Namespace", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
+ {
+ b.Property("NodeAclRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("LdapGroup")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("NodeAclId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("PermissionFlags")
+ .HasColumnType("int");
+
+ b.Property("ScopeId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ScopeKind")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.HasKey("NodeAclRowId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_NodeAcl_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "LdapGroup")
+ .HasDatabaseName("IX_NodeAcl_Generation_Group");
+
+ b.HasIndex("GenerationId", "NodeAclId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_Generation_LogicalId")
+ .HasFilter("[NodeAclId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ScopeKind", "ScopeId")
+ .HasDatabaseName("IX_NodeAcl_Generation_Scope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ClusterId", "LdapGroup", "ScopeKind", "ScopeId")
+ .IsUnique()
+ .HasDatabaseName("UX_NodeAcl_Generation_GroupScope")
+ .HasFilter("[ScopeId] IS NOT NULL");
+
+ b.ToTable("NodeAcl", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.PollGroup", b =>
+ {
+ b.Property("PollGroupRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("IntervalMs")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("PollGroupRowId");
+
+ b.HasIndex("GenerationId", "DriverInstanceId")
+ .HasDatabaseName("IX_PollGroup_Generation_Driver");
+
+ b.HasIndex("GenerationId", "PollGroupId")
+ .IsUnique()
+ .HasDatabaseName("UX_PollGroup_Generation_LogicalId")
+ .HasFilter("[PollGroupId] IS NOT NULL");
+
+ b.ToTable("PollGroup", null, t =>
+ {
+ t.HasCheckConstraint("CK_PollGroup_IntervalMs_Min", "IntervalMs >= 50");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
+ {
+ b.Property("ClusterId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2(3)")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("Enterprise")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("ModifiedBy")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("NodeCount")
+ .HasColumnType("tinyint");
+
+ b.Property("Notes")
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)");
+
+ b.Property("RedundancyMode")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("Site")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.HasKey("ClusterId");
+
+ b.HasIndex("Name")
+ .IsUnique()
+ .HasDatabaseName("UX_ServerCluster_Name");
+
+ b.HasIndex("Site")
+ .HasDatabaseName("IX_ServerCluster_Site");
+
+ b.ToTable("ServerCluster", null, t =>
+ {
+ t.HasCheckConstraint("CK_ServerCluster_RedundancyMode_NodeCount", "((NodeCount = 1 AND RedundancyMode = 'None') OR (NodeCount = 2 AND RedundancyMode IN ('Warm', 'Hot')))");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
+ {
+ b.Property("TagRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("AccessLevel")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("nvarchar(16)");
+
+ b.Property("DataType")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("DeviceId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("DriverInstanceId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("EquipmentId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("FolderPath")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("PollGroupId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("TagConfig")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("TagId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("WriteIdempotent")
+ .HasColumnType("bit");
+
+ b.HasKey("TagRowId");
+
+ b.HasIndex("GenerationId", "EquipmentId")
+ .HasDatabaseName("IX_Tag_Generation_Equipment")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "TagId")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_LogicalId")
+ .HasFilter("[TagId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId", "DeviceId")
+ .HasDatabaseName("IX_Tag_Generation_Driver_Device");
+
+ b.HasIndex("GenerationId", "EquipmentId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_EquipmentPath")
+ .HasFilter("[EquipmentId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "DriverInstanceId", "FolderPath", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_Tag_Generation_FolderPath")
+ .HasFilter("[EquipmentId] IS NULL");
+
+ b.ToTable("Tag", null, t =>
+ {
+ t.HasCheckConstraint("CK_Tag_TagConfig_IsJson", "ISJSON(TagConfig) = 1");
+ });
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
+ {
+ b.Property("UnsAreaRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("UnsAreaId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsAreaRowId");
+
+ b.HasIndex("ClusterId");
+
+ b.HasIndex("GenerationId", "ClusterId")
+ .HasDatabaseName("IX_UnsArea_Generation_Cluster");
+
+ b.HasIndex("GenerationId", "UnsAreaId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_Generation_LogicalId")
+ .HasFilter("[UnsAreaId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "ClusterId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsArea_Generation_ClusterName");
+
+ b.ToTable("UnsArea", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsLine", b =>
+ {
+ b.Property("UnsLineRowId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier")
+ .HasDefaultValueSql("NEWSEQUENTIALID()");
+
+ b.Property("GenerationId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("Notes")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("UnsAreaId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("UnsLineId")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.HasKey("UnsLineRowId");
+
+ b.HasIndex("GenerationId", "UnsAreaId")
+ .HasDatabaseName("IX_UnsLine_Generation_Area");
+
+ b.HasIndex("GenerationId", "UnsLineId")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_Generation_LogicalId")
+ .HasFilter("[UnsLineId] IS NOT NULL");
+
+ b.HasIndex("GenerationId", "UnsAreaId", "Name")
+ .IsUnique()
+ .HasDatabaseName("UX_UnsLine_Generation_AreaName");
+
+ b.ToTable("UnsLine", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Nodes")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeCredential", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", "Node")
+ .WithMany("Credentials")
+ .HasForeignKey("NodeId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "CurrentGeneration")
+ .WithMany()
+ .HasForeignKey("CurrentGenerationId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", "Node")
+ .WithOne("GenerationState")
+ .HasForeignKey("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNodeGenerationState", "NodeId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("CurrentGeneration");
+
+ b.Navigation("Node");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Generations")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Parent")
+ .WithMany()
+ .HasForeignKey("ParentGenerationId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Device", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.DriverInstance", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Equipment", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", "Batch")
+ .WithMany("Rows")
+ .HasForeignKey("BatchId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Batch");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.LdapGroupRoleMapping", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Cluster");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Namespace", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany("Namespaces")
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.NodeAcl", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.PollGroup", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.Tag", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsArea", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", "Cluster")
+ .WithMany()
+ .HasForeignKey("ClusterId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Cluster");
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.UnsLine", b =>
+ {
+ b.HasOne("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ConfigGeneration", "Generation")
+ .WithMany()
+ .HasForeignKey("GenerationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Generation");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ClusterNode", b =>
+ {
+ b.Navigation("Credentials");
+
+ b.Navigation("GenerationState");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
+ {
+ b.Navigation("Rows");
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.ServerCluster", b =>
+ {
+ b.Navigation("Generations");
+
+ b.Navigation("Namespaces");
+
+ b.Navigation("Nodes");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.cs
new file mode 100644
index 0000000..0e76922
--- /dev/null
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20260419185124_AddEquipmentImportBatch.cs
@@ -0,0 +1,91 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
+{
+ ///
+ public partial class AddEquipmentImportBatch : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "EquipmentImportBatch",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false),
+ ClusterId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ CreatedBy = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ CreatedAtUtc = table.Column(type: "datetime2(3)", nullable: false),
+ RowsStaged = table.Column(type: "int", nullable: false),
+ RowsAccepted = table.Column(type: "int", nullable: false),
+ RowsRejected = table.Column(type: "int", nullable: false),
+ FinalisedAtUtc = table.Column(type: "datetime2(3)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_EquipmentImportBatch", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "EquipmentImportRow",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false),
+ BatchId = table.Column(type: "uniqueidentifier", nullable: false),
+ LineNumberInFile = table.Column(type: "int", nullable: false),
+ IsAccepted = table.Column(type: "bit", nullable: false),
+ RejectReason = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true),
+ ZTag = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ MachineCode = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ SAPID = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ EquipmentId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ EquipmentUuid = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ UnsAreaName = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ UnsLineName = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ Manufacturer = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ Model = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ SerialNumber = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true),
+ HardwareRevision = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true),
+ SoftwareRevision = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true),
+ YearOfConstruction = table.Column(type: "nvarchar(8)", maxLength: 8, nullable: true),
+ AssetLocation = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true),
+ ManufacturerUri = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true),
+ DeviceManualUri = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_EquipmentImportRow", x => x.Id);
+ table.ForeignKey(
+ name: "FK_EquipmentImportRow_EquipmentImportBatch_BatchId",
+ column: x => x.BatchId,
+ principalTable: "EquipmentImportBatch",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_EquipmentImportBatch_Creator_Finalised",
+ table: "EquipmentImportBatch",
+ columns: new[] { "CreatedBy", "FinalisedAtUtc" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_EquipmentImportRow_Batch",
+ table: "EquipmentImportRow",
+ column: "BatchId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "EquipmentImportRow");
+
+ migrationBuilder.DropTable(
+ name: "EquipmentImportBatch");
+ }
+ }
+}
diff --git a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
index b1dd394..1019e8e 100644
--- a/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
+++ b/src/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/OtOpcUaConfigDbContextModelSnapshot.cs
@@ -604,6 +604,148 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
b.ToTable("Equipment", (string)null);
});
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportBatch", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ClusterId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("FinalisedAtUtc")
+ .HasColumnType("datetime2(3)");
+
+ b.Property("RowsAccepted")
+ .HasColumnType("int");
+
+ b.Property("RowsRejected")
+ .HasColumnType("int");
+
+ b.Property("RowsStaged")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedBy", "FinalisedAtUtc")
+ .HasDatabaseName("IX_EquipmentImportBatch_Creator_Finalised");
+
+ b.ToTable("EquipmentImportBatch", (string)null);
+ });
+
+ modelBuilder.Entity("ZB.MOM.WW.OtOpcUa.Configuration.Entities.EquipmentImportRow", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("AssetLocation")
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property