Closes the server-side/data-layer piece of Phase 6.4 Stream B.2-B.4. The CSV-import preview + modal UI (Stream B.3/B.5) still belongs to the Admin UI follow-up — this PR owns the staging tables + atomic finalise alone. Configuration: - New EquipmentImportBatch entity (Id, ClusterId, CreatedBy, CreatedAtUtc, RowsStaged/Accepted/Rejected, FinalisedAtUtc?). Composite index on (CreatedBy, FinalisedAtUtc) powers the Admin preview modal's "my open batches" query. - New EquipmentImportRow entity — one row per CSV row, 8 required columns from decision #117 + 9 optional from decision #139 + IsAccepted flag + RejectReason. FK to EquipmentImportBatch with cascade delete so DropBatch collapses the whole tree. - EF migration 20260419_..._AddEquipmentImportBatch. - SchemaComplianceTests expected tables list gains the two new tables. Admin.Services.EquipmentImportBatchService: - CreateBatchAsync — new header row, caller-supplied ClusterId + CreatedBy. - StageRowsAsync(batchId, acceptedRows, rejectedRows) — bulk-inserts the parsed CSV rows into staging. Rejected rows carry LineNumberInFile + RejectReason for the preview modal. Throws when the batch is finalised. - DropBatchAsync — removes batch + cascaded rows. Throws when the batch was already finalised (rollback via staging is not a time machine). - FinaliseBatchAsync(batchId, generationId, driverInstanceId, unsLineId) — atomic apply. Opens an EF transaction when the provider supports it (SQL Server in prod; InMemory in tests skips the tx), bulk-inserts every accepted staging row into Equipment, stamps EquipmentImportBatch.FinalisedAtUtc, commits. Failure rolls back so Equipment never partially mutates. Idempotent-under-double-call: second finalise throws ImportBatchAlreadyFinalisedException. - ListByUserAsync(createdBy, includeFinalised) — the Admin preview modal's backing query. OrderByDescending on CreatedAtUtc so the most-recent batch shows first. - Two exception types: ImportBatchNotFoundException + ImportBatchAlreadyFinalisedException. ExternalIdReservation merging (ZTag + SAPID fleet-wide uniqueness) is NOT done here — a narrower follow-up wires it once the concurrent-insert test matrix is green. Tests (10 new EquipmentImportBatchServiceTests, all pass): - CreateBatch populates Id + CreatedAtUtc + zero-ed counters. - StageRows accepted + rejected both persist; counters advance. - DropBatch cascades row delete. - DropBatch after finalise throws. - Finalise translates accepted staging rows → Equipment under the target GenerationId + DriverInstanceId + UnsLineId. - Finalise twice throws. - Finalise of unknown batch throws. - Stage after finalise throws. - ListByUserAsync filters by creator + finalised flag. - Drop of unknown batch is a no-op (idempotent rollback). Full solution dotnet test: 1235 passing (was 1225, +10). Pre-existing Client.CLI Subscribe flake unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
5.0 KiB
C#
92 lines
5.0 KiB
C#
using System;
|
|
using Microsoft.EntityFrameworkCore.Migrations;
|
|
|
|
#nullable disable
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
|
{
|
|
/// <inheritdoc />
|
|
public partial class AddEquipmentImportBatch : Migration
|
|
{
|
|
/// <inheritdoc />
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.CreateTable(
|
|
name: "EquipmentImportBatch",
|
|
columns: table => new
|
|
{
|
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
|
ClusterId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
|
CreatedBy = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
|
CreatedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: false),
|
|
RowsStaged = table.Column<int>(type: "int", nullable: false),
|
|
RowsAccepted = table.Column<int>(type: "int", nullable: false),
|
|
RowsRejected = table.Column<int>(type: "int", nullable: false),
|
|
FinalisedAtUtc = table.Column<DateTime>(type: "datetime2(3)", nullable: true)
|
|
},
|
|
constraints: table =>
|
|
{
|
|
table.PrimaryKey("PK_EquipmentImportBatch", x => x.Id);
|
|
});
|
|
|
|
migrationBuilder.CreateTable(
|
|
name: "EquipmentImportRow",
|
|
columns: table => new
|
|
{
|
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
|
BatchId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
|
LineNumberInFile = table.Column<int>(type: "int", nullable: false),
|
|
IsAccepted = table.Column<bool>(type: "bit", nullable: false),
|
|
RejectReason = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
|
ZTag = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
|
MachineCode = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
|
SAPID = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
|
EquipmentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
|
EquipmentUuid = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
|
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
|
UnsAreaName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
|
UnsLineName = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
|
Manufacturer = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
|
Model = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
|
SerialNumber = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
|
|
HardwareRevision = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
|
SoftwareRevision = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
|
|
YearOfConstruction = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: true),
|
|
AssetLocation = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
|
ManufacturerUri = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
|
DeviceManualUri = table.Column<string>(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");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Down(MigrationBuilder migrationBuilder)
|
|
{
|
|
migrationBuilder.DropTable(
|
|
name: "EquipmentImportRow");
|
|
|
|
migrationBuilder.DropTable(
|
|
name: "EquipmentImportBatch");
|
|
}
|
|
}
|
|
}
|