From 859fa4a42a5be04e495ff67be39ce840aa65cb04 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 26 Jul 2026 12:11:05 +0200 Subject: [PATCH] Decouple legacy migration service --- AGENTS.md | 3 + docs/current-architecture.md | 3 + docs/spec/07-implementation-phases-todo.md | 3 +- package.json | 4 +- scripts/db-migrate-legacy-consumers.ts | 13 +- .../legacy-consumer-migration.repository.ts | 37 +---- .../ports/legacy-consumer-migration.store.ts | 143 ++++++++++++++++++ .../legacy-consumer-migration.service.ts | 70 +++++---- .../legacy-consumer-migration.service.test.ts | 130 ++++++++++++++++ 9 files changed, 335 insertions(+), 71 deletions(-) create mode 100644 src/domain/ports/legacy-consumer-migration.store.ts create mode 100644 tests/legacy-consumer-migration.service.test.ts diff --git a/AGENTS.md b/AGENTS.md index e14e3f3..d7682e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,9 @@ It must support circuits, device rows, project devices, drag-and-drop restructur directly; do not reintroduce a standalone runtime revision repository. - Runtime domain services receive narrow reader/store dependencies explicitly; concrete SQLite repositories are instantiated only under `src/server/composition`. +- The upgrade-only legacy migration service follows the same dependency rule; + its concrete repositories are instantiated only in + `scripts/db-migrate-legacy-consumers.ts`. - General Circuit, CircuitDeviceRow, CircuitList and DistributionBoard repositories expose only active reads. Runtime writes belong in typed command repositories; direct integration fixtures belong under `tests/support`. diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 7acf29f..af37d9c 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -249,6 +249,9 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. - `npm run db:backup` erzeugt ein konsistentes und verifiziertes Online-Backup. - Angewendete Migrationen werden niemals nachträglich verändert. - `db:migrate:legacy-consumers` ist Upgrade-Werkzeug, kein Anwendungspfad. + Der fachliche Migrationsdienst kennt nur schmale Reader-/Store-Ports unter + `src/domain/ports`; konkrete SQLite-Repositories werden ausschließlich im + CLI-Skript zusammengesetzt. - Allgemeine Circuit-, Gerätezeilen-, CircuitList- und DistributionBoard- Repositories stellen im Anwendungspfad nur noch benötigte Leseabfragen bereit. Fachliche Schreibvorgänge liegen in den typisierten Command-Repositories; diff --git a/docs/spec/07-implementation-phases-todo.md b/docs/spec/07-implementation-phases-todo.md index ef50e0b..181457a 100644 --- a/docs/spec/07-implementation-phases-todo.md +++ b/docs/spec/07-implementation-phases-todo.md @@ -340,7 +340,8 @@ Implemented foundation: - circuit-device-row transaction tests cover both successful commits and forced SQLite rollbacks - project-device row synchronization has real multi-row SQLite commit and rollback coverage - BMK swaps and section reorder have real SQLite commit and rollback coverage -- the legacy consumer migration receives its database dependency at the script entry point +- the legacy consumer migration service depends only on narrow domain ports; + all concrete repositories are composed at the script entry point - legacy circuit, row, mapping and report writes have real late-failure rollback coverage - circuit and device-row persistence value mapping is separated from the general repositories - obsolete direct Circuit, CircuitDeviceRow, CircuitList, DistributionBoard and diff --git a/package.json b/package.json index dec1a70..9b0851f 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "build:api": "tsc -p tsconfig.json", "build:web": "next build", "start": "node dist/server/index.js", - "test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", - "test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test": "tsx --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", + "test:watch": "tsx --watch --test tests/project-device-schema.test.ts tests/project-device-schema-migration.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/legacy-consumer-migration.service.test.ts tests/legacy-consumer-migration.repository.test.ts tests/circuit-numbering.service.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-editor-history.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-structure-command.test.ts tests/circuit-device-row-move-command.test.ts tests/circuit-section-reorder-command.test.ts tests/circuit-section-renumber-command.test.ts tests/circuit-grid-projection.test.ts tests/distribution-board-structure-project-command.repository.test.ts tests/project-location-structure-project-command.repository.test.ts tests/project-device-project-command.repository.test.ts tests/project-device-structure-project-command.repository.test.ts tests/project-device-row-sync-project-command.repository.test.ts tests/circuit-section-reorder-project-command.repository.test.ts tests/circuit-section-renumber-project-command.repository.test.ts tests/project-command.model.test.ts tests/project-revision.persistence.test.ts tests/circuit-project-command.repository.test.ts tests/circuit-structure-project-command.repository.test.ts tests/circuit-device-row-project-command.repository.test.ts tests/circuit-device-row-structure-project-command.repository.test.ts tests/circuit-device-row-move-project-command.repository.test.ts tests/project-history.repository.test.ts tests/project-history-timeline.test.ts tests/project-version-history.test.ts tests/project-settings-project-command.repository.test.ts tests/project-state-snapshot.test.ts tests/project-snapshot-policy.test.ts tests/project-snapshot.repository.test.ts tests/project-state-restore-command.repository.test.ts tests/project-command.service.test.ts tests/database-backup.test.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:backup": "tsx scripts/db-backup.ts", diff --git a/scripts/db-migrate-legacy-consumers.ts b/scripts/db-migrate-legacy-consumers.ts index cabe2a9..f149e28 100644 --- a/scripts/db-migrate-legacy-consumers.ts +++ b/scripts/db-migrate-legacy-consumers.ts @@ -1,15 +1,22 @@ import { db } from "../src/db/client.js"; +import { CircuitRepository } from "../src/db/repositories/circuit.repository.js"; import { CircuitListRepository } from "../src/db/repositories/circuit-list.repository.js"; +import { CircuitSectionRepository } from "../src/db/repositories/circuit-section.repository.js"; import { LegacyConsumerMigrationRepository } from "../src/db/repositories/legacy-consumer-migration.repository.js"; import { ProjectRepository } from "../src/db/repositories/project.repository.js"; +import { RoomRepository } from "../src/db/repositories/room.repository.js"; import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js"; const projectRepository = new ProjectRepository(); const circuitListRepository = new CircuitListRepository(); const migrationRepository = new LegacyConsumerMigrationRepository(db); -const migrationService = new LegacyConsumerMigrationService( - migrationRepository -); +const migrationService = new LegacyConsumerMigrationService({ + circuitListReader: circuitListRepository, + sectionStore: new CircuitSectionRepository(), + circuitReader: new CircuitRepository(), + roomReader: new RoomRepository(), + migrationStore: migrationRepository, +}); async function run() { const projects = await projectRepository.list(); diff --git a/src/db/repositories/legacy-consumer-migration.repository.ts b/src/db/repositories/legacy-consumer-migration.repository.ts index e0896c7..2c3fa35 100644 --- a/src/db/repositories/legacy-consumer-migration.repository.ts +++ b/src/db/repositories/legacy-consumer-migration.repository.ts @@ -1,38 +1,17 @@ import crypto from "node:crypto"; import { eq, inArray, isNull } from "drizzle-orm"; +import type { + LegacyMigrationCircuitInput, + LegacyMigrationReportInput, +} from "../../domain/ports/legacy-consumer-migration.store.js"; import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuits } from "../schema/circuits.js"; import { consumers } from "../schema/consumers.js"; import { legacyConsumerCircuitMigrations } from "../schema/legacy-consumer-circuit-migrations.js"; import { legacyConsumerMigrationReports } from "../schema/legacy-consumer-migration-report.js"; -import { - toCircuitDeviceRowCreateValues, - type CircuitDeviceRowCreateInput, -} from "./circuit-device-row.persistence.js"; -import { - toCircuitCreateValues, - type CircuitCreatePersistenceInput, -} from "./circuit.persistence.js"; - -export interface LegacyMigrationCircuitPersistenceInput { - circuit: CircuitCreatePersistenceInput; - deviceRows: Array< - Omit & { legacyConsumerId: string } - >; -} - -export interface LegacyMigrationReportPersistenceInput { - legacyConsumerCount: number; - createdCircuitCount: number; - createdDeviceRowCount: number; - duplicateGroupedCount: number; - generatedIdentifierCount: number; - unassignedRowCount: number; - warningsJson: string; - generatedIdentifiersJson: string; - duplicateGroupsJson: string; -} +import { toCircuitDeviceRowCreateValues } from "./circuit-device-row.persistence.js"; +import { toCircuitCreateValues } from "./circuit.persistence.js"; export class LegacyConsumerMigrationRepository { constructor(private readonly database: AppDatabase) {} @@ -69,8 +48,8 @@ export class LegacyConsumerMigrationRepository { persistCircuitListMigration(input: { circuitListId: string; - circuits: LegacyMigrationCircuitPersistenceInput[]; - report: LegacyMigrationReportPersistenceInput; + circuits: LegacyMigrationCircuitInput[]; + report: LegacyMigrationReportInput; }) { if (input.circuits.some((entry) => entry.circuit.circuitListId !== input.circuitListId)) { throw new Error("All migrated circuits must belong to the target circuit list."); diff --git a/src/domain/ports/legacy-consumer-migration.store.ts b/src/domain/ports/legacy-consumer-migration.store.ts new file mode 100644 index 0000000..13d0904 --- /dev/null +++ b/src/domain/ports/legacy-consumer-migration.store.ts @@ -0,0 +1,143 @@ +export interface LegacyConsumerMigrationCircuitListReader { + findById( + projectId: string, + circuitListId: string + ): Promise<{ id: string } | null>; +} + +export interface LegacyConsumerMigrationSection { + id: string; + key: string; + prefix: string; +} + +export interface LegacyConsumerMigrationSectionStore { + createDefaults(circuitListId: string): Promise; + listByCircuitList( + circuitListId: string + ): Promise; +} + +export interface LegacyConsumerMigrationCircuitReader { + listByCircuitList(circuitListId: string): Promise< + Array<{ + sectionId: string; + equipmentIdentifier: string; + sortOrder: number; + }> + >; +} + +export interface LegacyConsumerMigrationRoomReader { + listByProject(projectId: string): Promise< + Array<{ + id: string; + roomNumber: string; + roomName: string; + }> + >; +} + +export interface LegacyConsumerSource { + id: string; + projectDeviceId: string | null; + roomId: string | null; + circuitNumber: string | null; + description: string | null; + name: string; + category: string | null; + deviceType: string | null; + phaseType: string | null; + tradeOrCostGroup: string | null; + protectionType: string | null; + protectionRatedCurrent: number | null; + protectionCharacteristic: string | null; + cableType: string | null; + cableCrossSection: string | null; + comment: string | null; + quantity: number; + installedPowerPerUnitKw: number; + demandFactor: number; + voltageV: number | null; + phaseCount: number | null; + powerFactor: number | null; + note: string | null; +} + +export interface LegacyMigrationDeviceRowInput { + linkedProjectDeviceId?: string; + legacyConsumerId: string; + sortOrder: number; + name: string; + displayName: string; + phaseType?: string; + connectionKind?: string; + costGroup?: string; + category?: string; + level?: string; + roomId?: string; + roomNumberSnapshot?: string; + roomNameSnapshot?: string; + quantity: number; + powerPerUnit: number; + simultaneityFactor: number; + cosPhi?: number; + remark?: string; + overriddenFields?: string; +} + +export interface LegacyMigrationCircuitInput { + circuit: { + circuitListId: string; + sectionId: string; + equipmentIdentifier: string; + displayName?: string; + sortOrder: number; + protectionType?: string; + protectionRatedCurrent?: number; + protectionCharacteristic?: string; + cableType?: string; + cableCrossSection?: string; + cableLength?: number; + voltage?: number; + controlRequirement?: string; + remark?: string; + rcdAssignment?: string; + terminalDesignation?: string; + status?: string; + isReserve?: boolean; + }; + deviceRows: LegacyMigrationDeviceRowInput[]; +} + +export interface LegacyMigrationReportInput { + legacyConsumerCount: number; + createdCircuitCount: number; + createdDeviceRowCount: number; + duplicateGroupedCount: number; + generatedIdentifierCount: number; + unassignedRowCount: number; + warningsJson: string; + generatedIdentifiersJson: string; + duplicateGroupsJson: string; +} + +export interface LegacyConsumerMigrationStore { + listSourceConsumersByCircuitList( + circuitListId: string + ): Promise; + listMigratedConsumerIds(circuitListId: string): Promise; + persistCircuitListMigration(input: { + circuitListId: string; + circuits: LegacyMigrationCircuitInput[]; + report: LegacyMigrationReportInput; + }): void; +} + +export interface LegacyConsumerMigrationDependencies { + circuitListReader: LegacyConsumerMigrationCircuitListReader; + sectionStore: LegacyConsumerMigrationSectionStore; + circuitReader: LegacyConsumerMigrationCircuitReader; + roomReader: LegacyConsumerMigrationRoomReader; + migrationStore: LegacyConsumerMigrationStore; +} diff --git a/src/domain/services/legacy-consumer-migration.service.ts b/src/domain/services/legacy-consumer-migration.service.ts index 747a390..c717d00 100644 --- a/src/domain/services/legacy-consumer-migration.service.ts +++ b/src/domain/services/legacy-consumer-migration.service.ts @@ -1,25 +1,19 @@ -import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; -import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js"; -import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; -import { - LegacyConsumerMigrationRepository, - type LegacyMigrationCircuitPersistenceInput, -} from "../../db/repositories/legacy-consumer-migration.repository.js"; -import { RoomRepository } from "../../db/repositories/room.repository.js"; import type { LegacyMigrationReport } from "../models/circuit-tree.model.js"; +import type { + LegacyConsumerMigrationDependencies, + LegacyConsumerSource, + LegacyMigrationCircuitInput, +} from "../ports/legacy-consumer-migration.store.js"; import { inferSectionKeyFromEquipmentIdentifier, inferSectionKeyFromLegacyInput, normalizeCircuitNumber, } from "./legacy-consumer-migration-planner.js"; -type LegacyConsumerRow = Awaited< - ReturnType< - LegacyConsumerMigrationRepository["listSourceConsumersByCircuitList"] - > ->[number]; - -function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): number | null { +function parseEquipmentSequence( + equipmentIdentifier: string, + prefix: string +): number | null { if (!equipmentIdentifier.startsWith(prefix)) { return null; } @@ -30,43 +24,45 @@ function parseEquipmentSequence(equipmentIdentifier: string, prefix: string): nu return Number(suffix); } - export class LegacyConsumerMigrationService { - private readonly circuitListRepository = new CircuitListRepository(); - private readonly sectionRepository = new CircuitSectionRepository(); - private readonly circuitRepository = new CircuitRepository(); - private readonly roomRepository = new RoomRepository(); - constructor( - private readonly migrationRepository: LegacyConsumerMigrationRepository + private readonly dependencies: LegacyConsumerMigrationDependencies ) {} - async migrateCircuitList(projectId: string, circuitListId: string): Promise { + async migrateCircuitList( + projectId: string, + circuitListId: string + ): Promise { // Migration is additive: legacy consumers are preserved, and circuit-first entities are created // with mapping records so transition remains auditable and reversible. - const list = await this.circuitListRepository.findById(projectId, circuitListId); + const list = await this.dependencies.circuitListReader.findById( + projectId, + circuitListId + ); if (!list) { throw new Error("Circuit list not found in project."); } - await this.sectionRepository.createDefaults(circuitListId); - const sections = await this.sectionRepository.listByCircuitList(circuitListId); + await this.dependencies.sectionStore.createDefaults(circuitListId); + const sections = + await this.dependencies.sectionStore.listByCircuitList(circuitListId); const sectionByKey = new Map(sections.map((section) => [section.key, section])); const unassignedSection = sectionByKey.get("unassigned"); if (!unassignedSection) { throw new Error("Unassigned section is required."); } - const existingCircuits = await this.circuitRepository.listByCircuitList(circuitListId); + const existingCircuits = + await this.dependencies.circuitReader.listByCircuitList(circuitListId); const usedEquipmentIdentifiers = new Set( existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase()) ); const legacyConsumers = - await this.migrationRepository.listSourceConsumersByCircuitList( + await this.dependencies.migrationStore.listSourceConsumersByCircuitList( circuitListId ); - const rooms = await this.roomRepository.listByProject(projectId); + const rooms = await this.dependencies.roomReader.listByProject(projectId); const roomById = new Map(rooms.map((room) => [room.id, room])); const report: LegacyMigrationReport = { @@ -82,14 +78,16 @@ export class LegacyConsumerMigrationService { // Idempotency guard: skip consumers already mapped in previous migration run. const migratedConsumerIds = new Set( - await this.migrationRepository.listMigratedConsumerIds(circuitListId) + await this.dependencies.migrationStore.listMigratedConsumerIds( + circuitListId + ) ); const consumersToMigrate = legacyConsumers.filter((consumer) => !migratedConsumerIds.has(consumer.id)); // Legacy rows are grouped by normalized circuit number so duplicates become // multiple device rows within one circuit instead of duplicate circuits. - const byNormalizedCircuitNumber = new Map(); - const withoutNormalizedCircuitNumber: LegacyConsumerRow[] = []; + const byNormalizedCircuitNumber = new Map(); + const withoutNormalizedCircuitNumber: LegacyConsumerSource[] = []; for (const consumer of consumersToMigrate) { const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null); if (!normalized) { @@ -110,7 +108,7 @@ export class LegacyConsumerMigrationService { const groups: Array<{ equipmentIdentifier: string | null; - consumers: LegacyConsumerRow[]; + consumers: LegacyConsumerSource[]; inferredSectionKey: string | null; isGeneratedIdentifier: boolean; }> = []; @@ -152,7 +150,7 @@ export class LegacyConsumerMigrationService { maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence)); } - const migrationCircuits: LegacyMigrationCircuitPersistenceInput[] = []; + const migrationCircuits: LegacyMigrationCircuitInput[] = []; for (const group of groups) { const representative = group.consumers[0]; let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null; @@ -181,7 +179,7 @@ export class LegacyConsumerMigrationService { usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase()); - const deviceRows: LegacyMigrationCircuitPersistenceInput["deviceRows"] = []; + const deviceRows: LegacyMigrationCircuitInput["deviceRows"] = []; const circuitSortOrder = nextSortOrder; nextSortOrder += 10; @@ -250,7 +248,7 @@ export class LegacyConsumerMigrationService { report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`); } - this.migrationRepository.persistCircuitListMigration({ + this.dependencies.migrationStore.persistCircuitListMigration({ circuitListId, circuits: migrationCircuits, report: { diff --git a/tests/legacy-consumer-migration.service.test.ts b/tests/legacy-consumer-migration.service.test.ts new file mode 100644 index 0000000..b78aca7 --- /dev/null +++ b/tests/legacy-consumer-migration.service.test.ts @@ -0,0 +1,130 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import type { + LegacyConsumerMigrationDependencies, + LegacyMigrationCircuitInput, + LegacyMigrationReportInput, +} from "../src/domain/ports/legacy-consumer-migration.store.js"; +import { LegacyConsumerMigrationService } from "../src/domain/services/legacy-consumer-migration.service.js"; + +describe("legacy consumer migration service", () => { + it("plans the migration through injected readers and persists one complete batch", async () => { + let defaultsCreatedFor: string | null = null; + let persisted: + | { + circuitListId: string; + circuits: LegacyMigrationCircuitInput[]; + report: LegacyMigrationReportInput; + } + | undefined; + const dependencies: LegacyConsumerMigrationDependencies = { + circuitListReader: { + async findById(projectId, circuitListId) { + assert.equal(projectId, "project-1"); + assert.equal(circuitListId, "list-1"); + return { id: circuitListId }; + }, + }, + sectionStore: { + async createDefaults(circuitListId) { + defaultsCreatedFor = circuitListId; + }, + async listByCircuitList() { + return [ + { id: "section-1", key: "single_phase", prefix: "-2F" }, + { id: "section-2", key: "unassigned", prefix: "-XF" }, + ]; + }, + }, + circuitReader: { + async listByCircuitList() { + return [ + { + sectionId: "section-1", + equipmentIdentifier: "-2F2", + sortOrder: 10, + }, + ]; + }, + }, + roomReader: { + async listByProject() { + return [ + { id: "room-1", roomNumber: "1.01", roomName: "Besprechung" }, + ]; + }, + }, + migrationStore: { + async listSourceConsumersByCircuitList() { + return [ + { + id: "consumer-1", + projectDeviceId: null, + roomId: "room-1", + circuitNumber: null, + description: "Steckdosen Besprechung", + name: "Steckdose", + category: null, + deviceType: null, + phaseType: null, + tradeOrCostGroup: "KG 440", + protectionType: null, + protectionRatedCurrent: null, + protectionCharacteristic: null, + cableType: null, + cableCrossSection: null, + comment: null, + quantity: 2, + installedPowerPerUnitKw: 0.2, + demandFactor: 0.8, + voltageV: 230, + phaseCount: 1, + powerFactor: 0.95, + note: "Bestand", + }, + ]; + }, + async listMigratedConsumerIds() { + return []; + }, + persistCircuitListMigration(input) { + persisted = input; + }, + }, + }; + + const report = await new LegacyConsumerMigrationService( + dependencies + ).migrateCircuitList("project-1", "list-1"); + + assert.equal(defaultsCreatedFor, "list-1"); + assert.deepEqual(report.generatedIdentifiers, ["-2F3"]); + assert.equal(report.createdCircuitCount, 1); + assert.equal(report.createdDeviceRowCount, 1); + assert.equal(persisted?.circuitListId, "list-1"); + assert.equal(persisted?.circuits[0].circuit.sectionId, "section-1"); + assert.equal( + persisted?.circuits[0].circuit.equipmentIdentifier, + "-2F3" + ); + assert.deepEqual(persisted?.circuits[0].deviceRows[0], { + linkedProjectDeviceId: undefined, + legacyConsumerId: "consumer-1", + sortOrder: 10, + name: "Steckdose", + displayName: "Steckdosen Besprechung", + phaseType: undefined, + connectionKind: undefined, + costGroup: "KG 440", + category: undefined, + roomId: "room-1", + roomNumberSnapshot: "1.01", + roomNameSnapshot: "Besprechung", + quantity: 2, + powerPerUnit: 0.2, + simultaneityFactor: 0.8, + cosPhi: 0.95, + remark: "Bestand", + }); + }); +});