Decouple legacy migration service
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<CircuitDeviceRowCreateInput, "circuitId"> & { 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.");
|
||||
|
||||
@@ -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<void>;
|
||||
listByCircuitList(
|
||||
circuitListId: string
|
||||
): Promise<LegacyConsumerMigrationSection[]>;
|
||||
}
|
||||
|
||||
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<LegacyConsumerSource[]>;
|
||||
listMigratedConsumerIds(circuitListId: string): Promise<string[]>;
|
||||
persistCircuitListMigration(input: {
|
||||
circuitListId: string;
|
||||
circuits: LegacyMigrationCircuitInput[];
|
||||
report: LegacyMigrationReportInput;
|
||||
}): void;
|
||||
}
|
||||
|
||||
export interface LegacyConsumerMigrationDependencies {
|
||||
circuitListReader: LegacyConsumerMigrationCircuitListReader;
|
||||
sectionStore: LegacyConsumerMigrationSectionStore;
|
||||
circuitReader: LegacyConsumerMigrationCircuitReader;
|
||||
roomReader: LegacyConsumerMigrationRoomReader;
|
||||
migrationStore: LegacyConsumerMigrationStore;
|
||||
}
|
||||
@@ -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<LegacyMigrationReport> {
|
||||
async migrateCircuitList(
|
||||
projectId: string,
|
||||
circuitListId: string
|
||||
): Promise<LegacyMigrationReport> {
|
||||
// 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<string, LegacyConsumerRow[]>();
|
||||
const withoutNormalizedCircuitNumber: LegacyConsumerRow[] = [];
|
||||
const byNormalizedCircuitNumber = new Map<string, LegacyConsumerSource[]>();
|
||||
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: {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user