Create clean release database baseline
This commit is contained in:
parent
5bee3cb103
commit
734a2bcfc4
129 changed files with 2121 additions and 30607 deletions
|
|
@ -1,60 +0,0 @@
|
|||
export interface LegacyConsumerForPlanning {
|
||||
id: string;
|
||||
circuitNumber: string | null;
|
||||
category: string | null;
|
||||
phaseType: string | null;
|
||||
phaseCount: number | null;
|
||||
}
|
||||
|
||||
// Accepts only normalized BMK-like legacy circuit numbers used for grouping.
|
||||
export function normalizeCircuitNumber(value: string | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim().toUpperCase();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (!/^-\d+F\d+$/.test(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// Best-effort fallback when no valid circuit number exists. Keeps migration deterministic
|
||||
// by preferring explicit category/phase cues over random assignment.
|
||||
export function inferSectionKeyFromLegacyInput(consumer: LegacyConsumerForPlanning): string | null {
|
||||
const category = (consumer.category ?? "").toLowerCase();
|
||||
if (category.includes("light") || category.includes("beleuchtung")) {
|
||||
return "lighting";
|
||||
}
|
||||
if (consumer.phaseCount === 3) {
|
||||
return "three_phase";
|
||||
}
|
||||
if (consumer.phaseCount === 1) {
|
||||
return "single_phase";
|
||||
}
|
||||
|
||||
const phaseType = (consumer.phaseType ?? "").toLowerCase();
|
||||
if (phaseType.includes("three") || phaseType.includes("3")) {
|
||||
return "three_phase";
|
||||
}
|
||||
if (phaseType.includes("single") || phaseType.includes("1")) {
|
||||
return "single_phase";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefix-based section inference for already normalized equipment identifiers.
|
||||
export function inferSectionKeyFromEquipmentIdentifier(equipmentIdentifier: string): string | null {
|
||||
if (equipmentIdentifier.startsWith("-1F")) {
|
||||
return "lighting";
|
||||
}
|
||||
if (equipmentIdentifier.startsWith("-2F")) {
|
||||
return "single_phase";
|
||||
}
|
||||
if (equipmentIdentifier.startsWith("-3F")) {
|
||||
return "three_phase";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
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";
|
||||
|
||||
function parseEquipmentSequence(
|
||||
equipmentIdentifier: string,
|
||||
prefix: string
|
||||
): number | null {
|
||||
if (!equipmentIdentifier.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
const suffix = equipmentIdentifier.slice(prefix.length);
|
||||
if (!/^\d+$/.test(suffix)) {
|
||||
return null;
|
||||
}
|
||||
return Number(suffix);
|
||||
}
|
||||
|
||||
export class LegacyConsumerMigrationService {
|
||||
constructor(
|
||||
private readonly dependencies: LegacyConsumerMigrationDependencies
|
||||
) {}
|
||||
|
||||
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.dependencies.circuitListReader.findById(
|
||||
projectId,
|
||||
circuitListId
|
||||
);
|
||||
if (!list) {
|
||||
throw new Error("Circuit list not found in project.");
|
||||
}
|
||||
|
||||
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.dependencies.circuitReader.listByCircuitList(circuitListId);
|
||||
const usedEquipmentIdentifiers = new Set(
|
||||
existingCircuits.map((circuit) => circuit.equipmentIdentifier.toUpperCase())
|
||||
);
|
||||
|
||||
const legacyConsumers =
|
||||
await this.dependencies.migrationStore.listSourceConsumersByCircuitList(
|
||||
circuitListId
|
||||
);
|
||||
const rooms = await this.dependencies.roomReader.listByProject(projectId);
|
||||
const roomById = new Map(rooms.map((room) => [room.id, room]));
|
||||
|
||||
const report: LegacyMigrationReport = {
|
||||
circuitListId,
|
||||
legacyConsumerCount: legacyConsumers.length,
|
||||
createdCircuitCount: 0,
|
||||
createdDeviceRowCount: 0,
|
||||
groupedDuplicateCircuitNumbers: [],
|
||||
generatedIdentifiers: [],
|
||||
unassignedRows: [],
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
// Idempotency guard: skip consumers already mapped in previous migration run.
|
||||
const migratedConsumerIds = new Set(
|
||||
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, LegacyConsumerSource[]>();
|
||||
const withoutNormalizedCircuitNumber: LegacyConsumerSource[] = [];
|
||||
for (const consumer of consumersToMigrate) {
|
||||
const normalized = normalizeCircuitNumber(consumer.circuitNumber ?? null);
|
||||
if (!normalized) {
|
||||
withoutNormalizedCircuitNumber.push(consumer);
|
||||
continue;
|
||||
}
|
||||
if (!byNormalizedCircuitNumber.has(normalized)) {
|
||||
byNormalizedCircuitNumber.set(normalized, []);
|
||||
}
|
||||
byNormalizedCircuitNumber.get(normalized)!.push(consumer);
|
||||
}
|
||||
|
||||
// Duplicate normalized circuit numbers are expected and represented as one circuit
|
||||
// with multiple circuit_device_rows.
|
||||
report.groupedDuplicateCircuitNumbers = [...byNormalizedCircuitNumber.entries()]
|
||||
.filter(([, grouped]) => grouped.length > 1)
|
||||
.map(([normalizedCircuitNumber, grouped]) => ({ normalizedCircuitNumber, count: grouped.length }));
|
||||
|
||||
const groups: Array<{
|
||||
equipmentIdentifier: string | null;
|
||||
consumers: LegacyConsumerSource[];
|
||||
inferredSectionKey: string | null;
|
||||
isGeneratedIdentifier: boolean;
|
||||
}> = [];
|
||||
|
||||
// Stable normalized circuit numbers keep existing intent where available.
|
||||
for (const [normalizedCircuitNumber, grouped] of byNormalizedCircuitNumber.entries()) {
|
||||
groups.push({
|
||||
equipmentIdentifier: normalizedCircuitNumber,
|
||||
consumers: grouped,
|
||||
inferredSectionKey: inferSectionKeyFromEquipmentIdentifier(normalizedCircuitNumber),
|
||||
isGeneratedIdentifier: false,
|
||||
});
|
||||
}
|
||||
|
||||
// Missing/invalid circuit numbers are migrated as single-row groups with
|
||||
// generated identifiers and best-effort section inference.
|
||||
for (const consumer of withoutNormalizedCircuitNumber) {
|
||||
groups.push({
|
||||
equipmentIdentifier: null,
|
||||
consumers: [consumer],
|
||||
inferredSectionKey: inferSectionKeyFromLegacyInput(consumer),
|
||||
isGeneratedIdentifier: true,
|
||||
});
|
||||
}
|
||||
|
||||
let nextSortOrder = existingCircuits.length ? Math.max(...existingCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||
|
||||
const maxBySectionPrefix = new Map<string, number>();
|
||||
for (const circuit of existingCircuits) {
|
||||
const section = sections.find((entry) => entry.id === circuit.sectionId);
|
||||
if (!section) {
|
||||
continue;
|
||||
}
|
||||
const sequence = parseEquipmentSequence(circuit.equipmentIdentifier.toUpperCase(), section.prefix.toUpperCase());
|
||||
if (sequence === null) {
|
||||
continue;
|
||||
}
|
||||
const current = maxBySectionPrefix.get(section.prefix.toUpperCase()) ?? 0;
|
||||
maxBySectionPrefix.set(section.prefix.toUpperCase(), Math.max(current, sequence));
|
||||
}
|
||||
|
||||
const migrationCircuits: LegacyMigrationCircuitInput[] = [];
|
||||
for (const group of groups) {
|
||||
const representative = group.consumers[0];
|
||||
let section = group.inferredSectionKey ? sectionByKey.get(group.inferredSectionKey) : null;
|
||||
if (!section) {
|
||||
section = unassignedSection;
|
||||
}
|
||||
|
||||
let equipmentIdentifier = group.equipmentIdentifier;
|
||||
if (!equipmentIdentifier || usedEquipmentIdentifiers.has(equipmentIdentifier.toUpperCase())) {
|
||||
const prefix = section.prefix.toUpperCase();
|
||||
const current = maxBySectionPrefix.get(prefix) ?? 0;
|
||||
const generatedSequence = current + 1;
|
||||
maxBySectionPrefix.set(prefix, generatedSequence);
|
||||
equipmentIdentifier = `${section.prefix}${generatedSequence}`;
|
||||
report.generatedIdentifiers.push(equipmentIdentifier);
|
||||
}
|
||||
|
||||
if (!group.inferredSectionKey && group.isGeneratedIdentifier) {
|
||||
for (const consumer of group.consumers) {
|
||||
report.unassignedRows.push({
|
||||
consumerId: consumer.id,
|
||||
reason: "Missing or invalid circuit number and no section inferred from phase/category.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
usedEquipmentIdentifiers.add(equipmentIdentifier.toUpperCase());
|
||||
|
||||
const deviceRows: LegacyMigrationCircuitInput["deviceRows"] = [];
|
||||
const circuitSortOrder = nextSortOrder;
|
||||
nextSortOrder += 10;
|
||||
|
||||
let rowSortOrder = 10;
|
||||
for (const consumer of group.consumers) {
|
||||
const room = consumer.roomId ? roomById.get(consumer.roomId) : undefined;
|
||||
let noteRemark = consumer.note?.trim() || consumer.comment?.trim() || undefined;
|
||||
if (consumer.deviceType && consumer.deviceType.trim()) {
|
||||
const legacyDeviceTypeNote = `Legacy deviceType: ${consumer.deviceType.trim()}`;
|
||||
noteRemark = noteRemark ? `${noteRemark} | ${legacyDeviceTypeNote}` : legacyDeviceTypeNote;
|
||||
}
|
||||
|
||||
deviceRows.push({
|
||||
linkedProjectDeviceId: consumer.projectDeviceId ?? undefined,
|
||||
legacyConsumerId: consumer.id,
|
||||
sortOrder: rowSortOrder,
|
||||
name: consumer.name,
|
||||
displayName: consumer.description ?? consumer.name,
|
||||
phaseType: consumer.phaseType ?? undefined,
|
||||
connectionKind: undefined,
|
||||
costGroup: consumer.tradeOrCostGroup ?? undefined,
|
||||
category: consumer.category ?? undefined,
|
||||
roomId: consumer.roomId ?? undefined,
|
||||
roomNumberSnapshot: room?.roomNumber,
|
||||
roomNameSnapshot: room?.roomName,
|
||||
quantity: consumer.quantity,
|
||||
powerPerUnit: consumer.installedPowerPerUnitKw,
|
||||
simultaneityFactor: consumer.demandFactor,
|
||||
cosPhi: consumer.powerFactor ?? undefined,
|
||||
remark: noteRemark,
|
||||
});
|
||||
rowSortOrder += 10;
|
||||
}
|
||||
|
||||
migrationCircuits.push({
|
||||
circuit: {
|
||||
circuitListId,
|
||||
sectionId: section.id,
|
||||
equipmentIdentifier,
|
||||
displayName: representative.description ?? representative.name,
|
||||
sortOrder: circuitSortOrder,
|
||||
protectionType: representative.protectionType ?? undefined,
|
||||
protectionRatedCurrent: representative.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: representative.protectionCharacteristic ?? undefined,
|
||||
cableType: representative.cableType ?? undefined,
|
||||
cableCrossSection: representative.cableCrossSection ?? undefined,
|
||||
voltage: representative.voltageV ?? undefined,
|
||||
remark: undefined,
|
||||
},
|
||||
deviceRows,
|
||||
});
|
||||
}
|
||||
report.createdCircuitCount = migrationCircuits.length;
|
||||
report.createdDeviceRowCount = migrationCircuits.reduce(
|
||||
(count, entry) => count + entry.deviceRows.length,
|
||||
0
|
||||
);
|
||||
|
||||
if (consumersToMigrate.some((consumer) => Boolean(consumer.comment?.trim()))) {
|
||||
report.warnings.push(
|
||||
"Legacy comment field was mapped to circuit_device_rows.remark because circuit-level vs row-level intent is ambiguous."
|
||||
);
|
||||
}
|
||||
|
||||
for (const row of report.unassignedRows) {
|
||||
report.warnings.push(`Consumer ${row.consumerId} was migrated into unassigned section.`);
|
||||
}
|
||||
|
||||
this.dependencies.migrationStore.persistCircuitListMigration({
|
||||
circuitListId,
|
||||
circuits: migrationCircuits,
|
||||
report: {
|
||||
legacyConsumerCount: report.legacyConsumerCount,
|
||||
createdCircuitCount: report.createdCircuitCount,
|
||||
createdDeviceRowCount: report.createdDeviceRowCount,
|
||||
duplicateGroupedCount: report.groupedDuplicateCircuitNumbers.length,
|
||||
generatedIdentifierCount: report.generatedIdentifiers.length,
|
||||
unassignedRowCount: report.unassignedRows.length,
|
||||
warningsJson: JSON.stringify(report.warnings),
|
||||
generatedIdentifiersJson: JSON.stringify(report.generatedIdentifiers),
|
||||
duplicateGroupsJson: JSON.stringify(report.groupedDuplicateCircuitNumbers),
|
||||
},
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,34 +6,6 @@ export function isElectricalPhaseType(
|
|||
return value === "single_phase" || value === "three_phase";
|
||||
}
|
||||
|
||||
export function normalizeKnownElectricalPhaseType(
|
||||
value: string | null
|
||||
): ElectricalPhaseType | null {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
const normalized = value
|
||||
.trim()
|
||||
.toLocaleLowerCase("de-DE")
|
||||
.replaceAll("-", "")
|
||||
.replaceAll("_", "");
|
||||
if (
|
||||
["1ph", "1phase", "1phasig", "einphasig", "singlephase"].includes(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return "single_phase";
|
||||
}
|
||||
if (
|
||||
["3ph", "3phase", "3phasig", "dreiphasig", "threephase"].includes(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return "three_phase";
|
||||
}
|
||||
throw new Error(`Unknown electrical phase type: ${value}`);
|
||||
}
|
||||
|
||||
export interface ProjectVoltageSettings {
|
||||
singlePhaseVoltageV: number;
|
||||
threePhaseVoltageV: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue