Phase 6.4 Stream B.2-B.4 server-side - EquipmentImportBatch staging + FinaliseBatch #106

Merged
dohertj2 merged 1 commits from phase-6-4-stream-b-staging-tables into v2 2026-04-19 14:57:41 -04:00
Owner

Ships staging tables + atomic finalise. CSV-import modal UI (Stream B.3/B.5) is the Admin UI follow-up.

Summary

  • EquipmentImportBatch + EquipmentImportRow entities + EF migration + composite index on (CreatedBy, FinalisedAtUtc).
  • EquipmentImportBatchService with Create/Stage/Drop/Finalise/ListByUser. Finalise is atomic — EF transaction on SQL Server, bulk-inserts accepted rows into Equipment under target GenerationId + DriverInstanceId + UnsLineId; failure rolls back.
  • Idempotent-under-double-finalise via ImportBatchAlreadyFinalisedException; pre-finalise rollback via DropBatch.
  • ExternalIdReservation merging deferred to a narrower follow-up.

Test plan

  • 10 new service tests: Create populates row; Stage persists accepted+rejected+counters; Drop cascades rows; Drop-after-finalise throws; Finalise promotes accepted rows to Equipment + stamps FinalisedAtUtc; double-finalise throws; unknown-batch throws; Stage-after-finalise throws; ListByUser filters; Drop-unknown is no-op.
  • SchemaComplianceTests expected-tables list gains the two new tables.
  • Full solution dotnet test: 1235 passing (was 1225, +10).

🤖 Generated with Claude Code

Ships staging tables + atomic finalise. CSV-import modal UI (Stream B.3/B.5) is the Admin UI follow-up. ## Summary - `EquipmentImportBatch` + `EquipmentImportRow` entities + EF migration + composite index on (CreatedBy, FinalisedAtUtc). - `EquipmentImportBatchService` with Create/Stage/Drop/Finalise/ListByUser. Finalise is atomic — EF transaction on SQL Server, bulk-inserts accepted rows into Equipment under target GenerationId + DriverInstanceId + UnsLineId; failure rolls back. - Idempotent-under-double-finalise via `ImportBatchAlreadyFinalisedException`; pre-finalise rollback via `DropBatch`. - ExternalIdReservation merging deferred to a narrower follow-up. ## Test plan - [x] 10 new service tests: Create populates row; Stage persists accepted+rejected+counters; Drop cascades rows; Drop-after-finalise throws; Finalise promotes accepted rows to Equipment + stamps FinalisedAtUtc; double-finalise throws; unknown-batch throws; Stage-after-finalise throws; ListByUser filters; Drop-unknown is no-op. - [x] SchemaComplianceTests expected-tables list gains the two new tables. - [x] Full solution `dotnet test`: 1235 passing (was 1225, +10). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
dohertj2 added 1 commit 2026-04-19 14:57:38 -04:00
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>
dohertj2 merged commit e032045247 into v2 2026-04-19 14:57:41 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#106