Create clean release database baseline

This commit is contained in:
2026-07-31 14:07:26 +02:00
parent 5bee3cb103
commit 734a2bcfc4
129 changed files with 2121 additions and 30607 deletions
@@ -10,7 +10,6 @@ export interface CircuitDeviceRowSnapshot {
id: string;
circuitId: string;
linkedProjectDeviceId: string | null;
legacyConsumerId: string | null;
sortOrder: number;
name: string;
displayName: string;
@@ -101,7 +100,6 @@ export function assertCircuitDeviceRowInsertProjectCommand(
assertNonEmptyString(row.id, "row.id");
assertNonEmptyString(row.circuitId, "row.circuitId");
assertNullableString(row.linkedProjectDeviceId, "row.linkedProjectDeviceId");
assertNullableString(row.legacyConsumerId, "row.legacyConsumerId");
assertFiniteNumber(row.sortOrder, "row.sortOrder");
assertNonEmptyString(row.name, "row.name");
assertNonEmptyString(row.displayName, "row.displayName");
@@ -2,7 +2,6 @@ export interface CircuitDeviceRow {
id: string;
circuitId: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
name: string;
displayName: string;
@@ -8,9 +8,6 @@ export interface CircuitUpdateValues {
equipmentIdentifier: string;
displayName: string | null;
sortOrder: number;
protectionType: string | null;
protectionRatedCurrent: number | null;
protectionCharacteristic: string | null;
cableType: string | null;
cableCrossSection: string | null;
cableLength: number | null;
@@ -118,7 +115,6 @@ function assertCircuitUpdateFieldValue(
return;
}
if (
field === "protectionRatedCurrent" ||
field === "cableLength" ||
field === "voltage"
) {
@@ -147,9 +143,6 @@ function isCircuitUpdateField(value: string): value is CircuitUpdateField {
"equipmentIdentifier",
"displayName",
"sortOrder",
"protectionType",
"protectionRatedCurrent",
"protectionCharacteristic",
"cableType",
"cableCrossSection",
"cableLength",
@@ -21,9 +21,6 @@ export interface CircuitSnapshot {
equipmentIdentifier: string;
displayName: string | null;
sortOrder: number;
protectionType: string | null;
protectionRatedCurrent: number | null;
protectionCharacteristic: string | null;
cableType: string | null;
cableCrossSection: string | null;
cableLength: number | null;
@@ -115,8 +112,6 @@ export function assertCircuitInsertProjectCommand(
assertNullableString(circuit.displayName, "circuit.displayName");
assertFiniteNumber(circuit.sortOrder, "circuit.sortOrder");
for (const field of [
"protectionType",
"protectionCharacteristic",
"cableType",
"cableCrossSection",
"rcdAssignment",
@@ -127,10 +122,6 @@ export function assertCircuitInsertProjectCommand(
] as const) {
assertNullableString(circuit[field], `circuit.${field}`);
}
assertNullableNonNegativeNumber(
circuit.protectionRatedCurrent,
"circuit.protectionRatedCurrent"
);
assertNullableNonNegativeNumber(
circuit.cableLength,
"circuit.cableLength"
-15
View File
@@ -1,7 +1,6 @@
export interface CircuitTreeDeviceRow {
id: string;
linkedProjectDeviceId?: string;
legacyConsumerId?: string;
sortOrder: number;
name: string;
displayName: string;
@@ -29,9 +28,6 @@ export interface CircuitTreeCircuit {
equipmentIdentifier: string;
displayName?: string;
sortOrder: number;
protectionType?: string;
protectionRatedCurrent?: number;
protectionCharacteristic?: string;
cableType?: string;
cableCrossSection?: string;
cableLength?: number;
@@ -73,17 +69,6 @@ export interface CircuitTreeResponse {
footerComponents: CircuitTreeComponent[];
}
export interface LegacyMigrationReport {
circuitListId: string;
legacyConsumerCount: number;
createdCircuitCount: number;
createdDeviceRowCount: number;
groupedDuplicateCircuitNumbers: Array<{ normalizedCircuitNumber: string; count: number }>;
generatedIdentifiers: string[];
unassignedRows: Array<{ consumerId: string; reason: string }>;
warnings: string[];
}
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
import type {
DistributionBoardComponentPlacement,
-3
View File
@@ -5,9 +5,6 @@ export interface Circuit {
equipmentIdentifier: string;
displayName?: string;
sortOrder: number;
protectionType?: string;
protectionRatedCurrent?: number;
protectionCharacteristic?: string;
cableType?: string;
cableCrossSection?: string;
cableLength?: number;
@@ -6,8 +6,7 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
export const distributionBoardUpdateCommandType =
"distribution-board.update" as const;
export const legacyDistributionBoardUpdateCommandSchemaVersion = 1 as const;
export const distributionBoardUpdateCommandSchemaVersion = 2 as const;
export const distributionBoardUpdateCommandSchemaVersion = 1 as const;
export interface DistributionBoardUpdateValues {
floorId: string | null;
@@ -33,27 +32,17 @@ export interface DistributionBoardUpdateCommandPayload {
changes: DistributionBoardUpdateFieldChange[];
}
interface CurrentDistributionBoardUpdateProjectCommand
export interface DistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof distributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
interface LegacyDistributionBoardUpdateProjectCommand
extends SerializedProjectCommand<DistributionBoardUpdateCommandPayload> {
schemaVersion: typeof legacyDistributionBoardUpdateCommandSchemaVersion;
type: typeof distributionBoardUpdateCommandType;
}
export type DistributionBoardUpdateProjectCommand =
| CurrentDistributionBoardUpdateProjectCommand
| LegacyDistributionBoardUpdateProjectCommand;
export function createDistributionBoardUpdateProjectCommand(
distributionBoardId: string,
patch: DistributionBoardUpdatePatch
): CurrentDistributionBoardUpdateProjectCommand {
const command: CurrentDistributionBoardUpdateProjectCommand = {
): DistributionBoardUpdateProjectCommand {
const command: DistributionBoardUpdateProjectCommand = {
schemaVersion: distributionBoardUpdateCommandSchemaVersion,
type: distributionBoardUpdateCommandType,
payload: {
@@ -72,9 +61,7 @@ export function assertDistributionBoardUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is DistributionBoardUpdateProjectCommand {
if (
(command.schemaVersion !==
legacyDistributionBoardUpdateCommandSchemaVersion &&
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion) ||
command.schemaVersion !== distributionBoardUpdateCommandSchemaVersion ||
command.type !== distributionBoardUpdateCommandType ||
!isPlainObject(command.payload)
) {
@@ -97,9 +84,7 @@ export function assertDistributionBoardUpdateProjectCommand(
!isPlainObject(change) ||
(change.field !== "floorId" &&
change.field !== "supplyType" &&
(command.schemaVersion ===
legacyDistributionBoardUpdateCommandSchemaVersion ||
change.field !== "simultaneityFactor")) ||
change.field !== "simultaneityFactor") ||
seen.has(change.field)
) {
throw new Error(
@@ -18,36 +18,7 @@ export const distributionBoardInsertCommandType =
"distribution-board.insert" as const;
export const distributionBoardDeleteCommandType =
"distribution-board.delete" as const;
export const legacyDistributionBoardStructureCommandSchemaVersion = 1 as const;
export const previousDistributionBoardStructureCommandSchemaVersion = 2 as const;
export const distributionBoardStructureCommandSchemaVersion = 3 as const;
const legacyDefaultCircuitSectionDefinitions = [
{
key: "lighting",
displayName: "Lighting",
prefix: "-1F",
sortOrder: 10,
},
{
key: "single_phase",
displayName: "Single-phase circuits",
prefix: "-2F",
sortOrder: 20,
},
{
key: "three_phase",
displayName: "Three-phase circuits",
prefix: "-3F",
sortOrder: 30,
},
{
key: "unassigned",
displayName: "Unassigned",
prefix: "-UF",
sortOrder: 90,
},
] as const;
export const distributionBoardStructureCommandSchemaVersion = 1 as const;
export const defaultCircuitSectionDefinitions = [
{
@@ -76,31 +47,6 @@ export const defaultCircuitSectionDefinitions = [
},
] as const;
interface LegacyDistributionBoardStructureSnapshot {
distributionBoard: {
id: string;
projectId: string;
name: string;
};
circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: LegacyCircuitSectionSnapshot[];
}
interface PreviousDistributionBoardStructureSnapshot {
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: LegacyCircuitSectionSnapshot[];
}
interface LegacyCircuitSectionSnapshot {
id: string;
circuitListId: string;
key: string;
displayName: string;
prefix: string;
sortOrder: number;
}
export interface DistributionBoardStructureSnapshot {
distributionBoard: {
id: string;
@@ -140,22 +86,15 @@ export interface DistributionBoardStructureSnapshot {
export interface NormalizedDistributionBoardStructureSnapshot {
distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"];
circuitList: DistributionBoardStructureSnapshot["circuitList"];
sections: Array<
LegacyCircuitSectionSnapshot & {
category: CircuitGroupCategory | null;
groupNumber: number | null;
}
>;
sections: DistributionBoardStructureSnapshot["sections"];
components: DistributionBoardStructureSnapshot["components"];
}
interface DistributionBoardStructureCommandPayload<
TStructure = DistributionBoardStructureSnapshot,
> {
structure: TStructure;
interface DistributionBoardStructureCommandPayload {
structure: DistributionBoardStructureSnapshot;
}
interface CurrentDistributionBoardStructureProjectCommand
interface DistributionBoardStructureProjectCommandBase
extends SerializedProjectCommand<DistributionBoardStructureCommandPayload> {
schemaVersion: typeof distributionBoardStructureCommandSchemaVersion;
type:
@@ -163,41 +102,19 @@ interface CurrentDistributionBoardStructureProjectCommand
| typeof distributionBoardDeleteCommandType;
}
interface LegacyDistributionBoardStructureProjectCommand
extends SerializedProjectCommand<
DistributionBoardStructureCommandPayload<LegacyDistributionBoardStructureSnapshot>
> {
schemaVersion: typeof legacyDistributionBoardStructureCommandSchemaVersion;
type:
| typeof distributionBoardInsertCommandType
| typeof distributionBoardDeleteCommandType;
}
interface PreviousDistributionBoardStructureProjectCommand
extends SerializedProjectCommand<
DistributionBoardStructureCommandPayload<PreviousDistributionBoardStructureSnapshot>
> {
schemaVersion: typeof previousDistributionBoardStructureCommandSchemaVersion;
type:
| typeof distributionBoardInsertCommandType
| typeof distributionBoardDeleteCommandType;
}
export interface DistributionBoardInsertProjectCommand
extends CurrentDistributionBoardStructureProjectCommand {
extends DistributionBoardStructureProjectCommandBase {
type: typeof distributionBoardInsertCommandType;
}
export interface DistributionBoardDeleteProjectCommand
extends CurrentDistributionBoardStructureProjectCommand {
extends DistributionBoardStructureProjectCommandBase {
type: typeof distributionBoardDeleteCommandType;
}
export type DistributionBoardStructureProjectCommand =
| DistributionBoardInsertProjectCommand
| DistributionBoardDeleteProjectCommand
| PreviousDistributionBoardStructureProjectCommand
| LegacyDistributionBoardStructureProjectCommand;
| DistributionBoardDeleteProjectCommand;
export function createDistributionBoardStructureSnapshot(
projectId: string,
@@ -296,35 +213,6 @@ export function assertDistributionBoardDeleteProjectCommand(
export function normalizeDistributionBoardStructureProjectCommand(
command: DistributionBoardStructureProjectCommand
): NormalizedDistributionBoardStructureSnapshot {
if (
command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
) {
return {
distributionBoard: {
...command.payload.structure.distributionBoard,
floorId: null,
supplyType: null,
},
circuitList: command.payload.structure.circuitList,
sections: command.payload.structure.sections.map(
normalizeLegacySection
),
components: [],
};
}
if (
command.schemaVersion ===
previousDistributionBoardStructureCommandSchemaVersion
) {
return {
...command.payload.structure,
sections: command.payload.structure.sections.map(
normalizeLegacySection
),
components: [],
};
}
return command.payload.structure;
}
@@ -340,26 +228,10 @@ export function invertDistributionBoardStructureProjectCommand(
} as DistributionBoardStructureProjectCommand;
}
function normalizeLegacySection(
section: LegacyCircuitSectionSnapshot
): NormalizedDistributionBoardStructureSnapshot["sections"][number] {
const category =
section.key === "lighting" ||
section.key === "single_phase" ||
section.key === "three_phase"
? section.key
: null;
return {
...section,
category,
groupNumber: category === null ? null : 1,
};
}
export function assertDistributionBoardStructureSnapshot(
structure: unknown
): asserts structure is DistributionBoardStructureSnapshot {
assertDistributionBoardStructureSnapshotVersion(structure, "current");
assertDistributionBoardStructureSnapshotVersion(structure);
}
function assertDistributionBoardStructureProjectCommand(
@@ -375,63 +247,36 @@ function assertDistributionBoardStructureProjectCommand(
) {
throw new Error("Unsupported distribution-board structure command.");
}
if (
command.schemaVersion ===
legacyDistributionBoardStructureCommandSchemaVersion
) {
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
"legacy"
);
return;
}
if (
command.schemaVersion ===
previousDistributionBoardStructureCommandSchemaVersion
) {
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
"previous"
);
return;
}
if (
command.schemaVersion !==
distributionBoardStructureCommandSchemaVersion
) {
throw new Error("Unsupported distribution-board structure command.");
}
assertDistributionBoardStructureSnapshotVersion(
command.payload.structure,
"current"
);
assertDistributionBoardStructureSnapshotVersion(command.payload.structure);
}
function assertDistributionBoardStructureSnapshotVersion(
structure: unknown,
version: "legacy" | "previous" | "current"
structure: unknown
) {
const current = version === "current";
if (
!isPlainObject(structure) ||
Object.keys(structure).length !== (current ? 4 : 3)
Object.keys(structure).length !== 4
) {
throw new Error("Distribution-board structure is invalid.");
}
const { distributionBoard, circuitList, sections, components } =
structure;
const expectedSections = current
? defaultCircuitSectionDefinitions
: legacyDefaultCircuitSectionDefinitions;
const expectedSections = defaultCircuitSectionDefinitions;
if (
!isPlainObject(distributionBoard) ||
Object.keys(distributionBoard).length !==
(version === "legacy" ? 3 : 5) ||
Object.keys(distributionBoard).length !== 5 ||
!isPlainObject(circuitList) ||
Object.keys(circuitList).length !== 4 ||
!Array.isArray(sections) ||
sections.length !== expectedSections.length ||
(current && (!Array.isArray(components) || components.length !== 2))
!Array.isArray(components) ||
components.length !== 2
) {
throw new Error("Distribution-board structure is incomplete.");
}
@@ -441,10 +286,8 @@ function assertDistributionBoardStructureSnapshotVersion(
`distributionBoard.${field}`
);
}
if (version !== "legacy") {
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
assertNullableSupplyType(distributionBoard.supplyType);
}
assertNullableId(distributionBoard.floorId, "distributionBoard.floorId");
assertNullableSupplyType(distributionBoard.supplyType);
for (const [field, value] of Object.entries(circuitList)) {
assertNonEmptyString(value, `circuitList.${field}`);
}
@@ -462,7 +305,7 @@ function assertDistributionBoardStructureSnapshotVersion(
const expected = expectedSections[index];
if (
!isPlainObject(section) ||
Object.keys(section).length !== (current ? 8 : 6)
Object.keys(section).length !== 8
) {
throw new Error("Default circuit section is invalid.");
}
@@ -477,10 +320,8 @@ function assertDistributionBoardStructureSnapshotVersion(
section.displayName !== expected.displayName ||
section.prefix !== expected.prefix ||
section.sortOrder !== expected.sortOrder ||
(current &&
(!("category" in expected) ||
section.category !== expected.category ||
section.groupNumber !== expected.groupNumber))
section.category !== expected.category ||
section.groupNumber !== expected.groupNumber
) {
throw new Error(
"Distribution-board structure has invalid default sections."
@@ -488,9 +329,7 @@ function assertDistributionBoardStructureSnapshotVersion(
}
}
if (current) {
assertDefaultComponents(components, circuitList.id);
}
assertDefaultComponents(components, circuitList.id);
}
function assertDefaultComponents(
@@ -156,7 +156,6 @@ export function cloneDistributionBoardSubtree(
...row,
id: createId(),
circuitId,
legacyConsumerId: null,
})),
protectionDevice:
circuit.protectionDevice === null ||
@@ -6,54 +6,29 @@ import {
export const projectSettingsUpdateCommandType =
"project.update-settings" as const;
export const legacyProjectSettingsUpdateCommandSchemaVersion = 1 as const;
export const previousProjectSettingsUpdateCommandSchemaVersion = 2 as const;
export const projectSettingsUpdateCommandSchemaVersion = 3 as const;
export const projectSettingsUpdateCommandSchemaVersion = 1 as const;
export interface LegacyProjectSettingsValues {
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
}
export interface PreviousProjectSettingsValues extends LegacyProjectSettingsValues {
export interface ProjectSettingsValues {
name: string;
internalProjectNumber: string | null;
externalProjectNumber: string | null;
buildingOwner: string | null;
description: string | null;
}
export interface ProjectSettingsValues extends PreviousProjectSettingsValues {
singlePhaseVoltageV: number;
threePhaseVoltageV: number;
enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[];
}
export interface LegacyProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<LegacyProjectSettingsValues> {
schemaVersion: typeof legacyProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export interface CurrentProjectSettingsUpdateProjectCommand
export interface ProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<ProjectSettingsValues> {
schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export interface PreviousProjectSettingsUpdateProjectCommand
extends SerializedProjectCommand<PreviousProjectSettingsValues> {
schemaVersion: typeof previousProjectSettingsUpdateCommandSchemaVersion;
type: typeof projectSettingsUpdateCommandType;
}
export type ProjectSettingsUpdateProjectCommand =
| LegacyProjectSettingsUpdateProjectCommand
| PreviousProjectSettingsUpdateProjectCommand
| CurrentProjectSettingsUpdateProjectCommand;
export function createProjectSettingsUpdateProjectCommand(
values: ProjectSettingsValues
): CurrentProjectSettingsUpdateProjectCommand {
const command: CurrentProjectSettingsUpdateProjectCommand = {
): ProjectSettingsUpdateProjectCommand {
const command: ProjectSettingsUpdateProjectCommand = {
schemaVersion: projectSettingsUpdateCommandSchemaVersion,
type: projectSettingsUpdateCommandType,
payload: { ...values },
@@ -67,11 +42,7 @@ export function assertProjectSettingsUpdateProjectCommand(
): asserts command is ProjectSettingsUpdateProjectCommand {
if (
command.type !== projectSettingsUpdateCommandType ||
(command.schemaVersion !==
legacyProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !==
previousProjectSettingsUpdateCommandSchemaVersion &&
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion)
command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion
) {
throw new Error("Unsupported project settings update command.");
}
@@ -80,14 +51,6 @@ export function assertProjectSettingsUpdateProjectCommand(
"Project settings update command contains invalid values."
);
}
if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) {
assertLegacyValues(command.payload);
return;
}
const expectedKeyCount =
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion
? 7
: 8;
if (
!isTrimmedString(command.payload.name, 1, 200) ||
!isNullableTrimmedString(command.payload.internalProjectNumber, 100) ||
@@ -96,14 +59,13 @@ export function assertProjectSettingsUpdateProjectCommand(
!isNullableTrimmedString(command.payload.description, 2000) ||
!isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) ||
!isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ||
Object.keys(command.payload).length !== expectedKeyCount
Object.keys(command.payload).length !== 8
) {
throw new Error(
"Project settings update command contains invalid values."
);
}
if (
command.schemaVersion === projectSettingsUpdateCommandSchemaVersion &&
!isValidSupplyTypes(
command.payload.enabledDistributionBoardSupplyTypes
)
@@ -114,28 +76,6 @@ export function assertProjectSettingsUpdateProjectCommand(
}
}
export function normalizeProjectSettingsValues(
command: ProjectSettingsUpdateProjectCommand,
current: ProjectSettingsValues
): ProjectSettingsValues {
if (command.schemaVersion === legacyProjectSettingsUpdateCommandSchemaVersion) {
return {
...current,
...command.payload,
};
}
if (
command.schemaVersion === previousProjectSettingsUpdateCommandSchemaVersion
) {
return {
...command.payload,
enabledDistributionBoardSupplyTypes:
current.enabledDistributionBoardSupplyTypes,
};
}
return command.payload;
}
function isValidSupplyTypes(
value: unknown
): value is DistributionBoardSupplyType[] {
@@ -153,20 +93,6 @@ function isValidSupplyTypes(
);
}
function assertLegacyValues(
payload: Record<string, unknown>
): asserts payload is Record<string, unknown> & LegacyProjectSettingsValues {
if (
!isPositiveFiniteNumber(payload.singlePhaseVoltageV) ||
!isPositiveFiniteNumber(payload.threePhaseVoltageV) ||
Object.keys(payload).length !== 2
) {
throw new Error(
"Project settings update command contains invalid voltages."
);
}
}
function isPositiveFiniteNumber(value: unknown): value is number {
return (
typeof value === "number" &&
+24 -304
View File
@@ -1,11 +1,7 @@
import { z } from "zod";
import {
defaultDistributionBoardSupplyTypes,
distributionBoardSupplyTypes,
} from "../../shared/constants/distribution-board.js";
import { distributionBoardSupplyTypes } from "../../shared/constants/distribution-board.js";
import {
circuitGroupCategories,
type CircuitGroupCategory,
} from "../../shared/constants/circuit-group.js";
import {
distributionBoardComponentPlacements,
@@ -21,61 +17,38 @@ import {
import {
resolveCircuitPhaseType,
resolveProjectVoltage,
normalizeKnownElectricalPhaseType,
} from "../services/project-voltage.service.js";
export const legacyProjectStateSnapshotSchemaVersion = 1 as const;
export const previousProjectStateSnapshotSchemaVersion = 2 as const;
export const distributionBoardProjectStateSnapshotSchemaVersion = 3 as const;
export const supplyTypesProjectStateSnapshotSchemaVersion = 4 as const;
export const voltageProjectStateSnapshotSchemaVersion = 5 as const;
export const simultaneityFactorProjectStateSnapshotSchemaVersion = 6 as const;
export const projectStateSnapshotSchemaVersion = 7 as const;
export const projectStateSnapshotSchemaVersion = 1 as const;
const idSchema = z.string().trim().min(1);
const nullableStringSchema = z.string().nullable();
const finiteNumberSchema = z.number().finite();
const legacyProjectSchema = z
const projectSchema = z
.object({
id: idSchema,
name: z.string().trim().min(1),
internalProjectNumber: z.string().trim().min(1).max(100).nullable(),
externalProjectNumber: z.string().trim().min(1).max(100).nullable(),
buildingOwner: z.string().trim().min(1).max(200).nullable(),
description: z.string().trim().min(1).max(2000).nullable(),
singlePhaseVoltageV: finiteNumberSchema.positive(),
threePhaseVoltageV: finiteNumberSchema.positive(),
enabledDistributionBoardSupplyTypes: z
.array(z.enum(distributionBoardSupplyTypes))
.min(1)
.refine((values) => new Set(values).size === values.length),
})
.strict();
const projectSchema = legacyProjectSchema.extend({
internalProjectNumber: z.string().trim().min(1).max(100).nullable(),
externalProjectNumber: z.string().trim().min(1).max(100).nullable(),
buildingOwner: z.string().trim().min(1).max(200).nullable(),
description: z.string().trim().min(1).max(2000).nullable(),
});
const currentProjectSchema = projectSchema.extend({
enabledDistributionBoardSupplyTypes: z
.array(z.enum(distributionBoardSupplyTypes))
.min(1)
.refine((values) => new Set(values).size === values.length),
});
const legacyDistributionBoardSchema = z
const distributionBoardSchema = z
.object({
id: idSchema,
projectId: idSchema,
name: z.string().trim().min(1),
})
.strict();
const distributionBoardSchema = legacyDistributionBoardSchema
.extend({
floorId: idSchema.nullable(),
supplyType: z.enum(distributionBoardSupplyTypes).nullable(),
})
.strict();
const currentDistributionBoardSchema = distributionBoardSchema
.extend({
simultaneityFactor: finiteNumberSchema.min(0).max(1),
})
.strict();
@@ -89,7 +62,7 @@ const circuitListSchema = z
})
.strict();
const legacyCircuitSectionSchema = z
const circuitSectionSchema = z
.object({
id: idSchema,
circuitListId: idSchema,
@@ -97,11 +70,6 @@ const legacyCircuitSectionSchema = z
displayName: z.string().trim().min(1),
prefix: z.string().trim().min(1),
sortOrder: finiteNumberSchema,
})
.strict();
const circuitSectionSchema = legacyCircuitSectionSchema
.extend({
category: z.enum(circuitGroupCategories).nullable(),
groupNumber: z.number().int().positive().nullable(),
})
@@ -121,11 +89,10 @@ const circuitDeviceRowSchema = z
id: idSchema,
circuitId: idSchema,
linkedProjectDeviceId: idSchema.nullable(),
legacyConsumerId: z.string().nullable(),
sortOrder: finiteNumberSchema,
name: z.string().trim().min(1),
displayName: z.string().trim().min(1),
phaseType: nullableStringSchema,
phaseType: z.enum(["single_phase", "three_phase"]).nullable(),
connectionKind: nullableStringSchema,
costGroup: nullableStringSchema,
category: nullableStringSchema,
@@ -150,9 +117,6 @@ const circuitSchema = z
equipmentIdentifier: z.string().trim().min(1),
displayName: nullableStringSchema,
sortOrder: finiteNumberSchema,
protectionType: nullableStringSchema,
protectionRatedCurrent: finiteNumberSchema.nonnegative().nullable(),
protectionCharacteristic: nullableStringSchema,
cableType: nullableStringSchema,
cableCrossSection: nullableStringSchema,
cableLength: finiteNumberSchema.nonnegative().nullable(),
@@ -280,18 +244,13 @@ const componentProtectionDeviceSchema = z
.strict()
.superRefine(validatePersistedProtectionDevice);
const legacyProjectStateSnapshotContents = {
const projectStateSnapshotContents = {
circuitLists: z.array(circuitListSchema),
circuitSections: z.array(legacyCircuitSectionSchema),
circuitSections: z.array(circuitSectionSchema),
circuits: z.array(circuitSchema),
projectDevices: z.array(projectDeviceSchema),
floors: z.array(floorSchema),
rooms: z.array(roomSchema),
};
const currentProjectStateSnapshotContents = {
...legacyProjectStateSnapshotContents,
circuitSections: z.array(circuitSectionSchema),
distributionBoardComponents: z.array(distributionBoardComponentSchema),
circuitProtectionDevices: z.array(circuitProtectionDeviceSchema),
distributionBoardComponentProtectionDevices: z.array(
@@ -299,64 +258,14 @@ const currentProjectStateSnapshotContents = {
),
};
const legacyProjectStateSnapshotSchema = z
export const projectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(legacyProjectStateSnapshotSchemaVersion),
project: legacyProjectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
...legacyProjectStateSnapshotContents,
})
.strict();
const previousProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(previousProjectStateSnapshotSchemaVersion),
project: projectSchema,
distributionBoards: z.array(legacyDistributionBoardSchema),
...legacyProjectStateSnapshotContents,
})
.strict();
const distributionBoardProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(
distributionBoardProjectStateSnapshotSchemaVersion
),
project: projectSchema,
distributionBoards: z.array(distributionBoardSchema),
...legacyProjectStateSnapshotContents,
})
.strict();
const supplyTypesProjectStateSnapshotSchema = z
.object({
schemaVersion: z.literal(supplyTypesProjectStateSnapshotSchemaVersion),
project: currentProjectSchema,
distributionBoards: z.array(distributionBoardSchema),
...legacyProjectStateSnapshotContents,
})
.strict();
const voltageProjectStateSnapshotSchema =
supplyTypesProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(
voltageProjectStateSnapshotSchemaVersion
),
});
const simultaneityFactorProjectStateSnapshotSchema =
voltageProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(
simultaneityFactorProjectStateSnapshotSchemaVersion
),
distributionBoards: z.array(currentDistributionBoardSchema),
});
export const projectStateSnapshotSchema =
simultaneityFactorProjectStateSnapshotSchema.extend({
schemaVersion: z.literal(projectStateSnapshotSchemaVersion),
...currentProjectStateSnapshotContents,
});
project: projectSchema,
distributionBoards: z.array(distributionBoardSchema),
...projectStateSnapshotContents,
})
.strict();
export type ProjectStateSnapshot = z.infer<
typeof projectStateSnapshotSchema
@@ -365,68 +274,7 @@ export type ProjectStateSnapshot = z.infer<
export function parseProjectStateSnapshot(
value: unknown
): ProjectStateSnapshot {
const version = isPlainObject(value) ? value.schemaVersion : undefined;
const parsedSnapshot =
version === legacyProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
upgradeLegacyProjectStateSnapshot(
legacyProjectStateSnapshotSchema.parse(value)
)
)
)
)
)
)
: version === previousProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
upgradePreviousProjectStateSnapshot(
previousProjectStateSnapshotSchema.parse(value)
)
)
)
)
)
: version === distributionBoardProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
upgradeDistributionBoardProjectStateSnapshot(
distributionBoardProjectStateSnapshotSchema.parse(
value
)
)
)
)
)
: version === supplyTypesProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
upgradeVoltageProjectStateSnapshot(
upgradeSupplyTypesProjectStateSnapshot(
supplyTypesProjectStateSnapshotSchema.parse(value)
)
)
)
: version === voltageProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
upgradeVoltageProjectStateSnapshot(
voltageProjectStateSnapshotSchema.parse(value)
)
)
: version ===
simultaneityFactorProjectStateSnapshotSchemaVersion
? upgradeSimultaneityFactorProjectStateSnapshot(
simultaneityFactorProjectStateSnapshotSchema.parse(
value
)
)
: projectStateSnapshotSchema.parse(value);
const parsedSnapshot = projectStateSnapshotSchema.parse(value);
const snapshot = normalizeSnapshotPhaseTypes(parsedSnapshot);
assertProjectStateSnapshotRelations(snapshot);
return snapshot;
@@ -446,15 +294,13 @@ function normalizeSnapshotPhaseTypes(
circuits: snapshot.circuits.map((circuit) => {
const section = sectionById.get(circuit.sectionId);
const deviceRows = circuit.deviceRows.map((row) => {
const normalizedPhaseType =
normalizeKnownElectricalPhaseType(row.phaseType);
const linkedPhaseType = row.linkedProjectDeviceId
? projectDeviceById.get(row.linkedProjectDeviceId)?.phaseType
: undefined;
return {
...row,
phaseType:
normalizedPhaseType ??
row.phaseType ??
linkedPhaseType ??
(section?.key === "three_phase"
? "three_phase"
@@ -478,132 +324,6 @@ function normalizeSnapshotPhaseTypes(
};
}
function upgradeLegacyProjectStateSnapshot(
snapshot: z.infer<typeof legacyProjectStateSnapshotSchema>
): z.infer<typeof previousProjectStateSnapshotSchema> {
return {
...snapshot,
schemaVersion: previousProjectStateSnapshotSchemaVersion,
project: {
...snapshot.project,
internalProjectNumber: null,
externalProjectNumber: null,
buildingOwner: null,
description: null,
},
};
}
function upgradePreviousProjectStateSnapshot(
snapshot: z.infer<typeof previousProjectStateSnapshotSchema>
): z.infer<typeof distributionBoardProjectStateSnapshotSchema> {
return {
...snapshot,
schemaVersion: distributionBoardProjectStateSnapshotSchemaVersion,
distributionBoards: snapshot.distributionBoards.map((board) => ({
...board,
floorId: null,
supplyType: null,
})),
};
}
function upgradeDistributionBoardProjectStateSnapshot(
snapshot: z.infer<typeof distributionBoardProjectStateSnapshotSchema>
): z.infer<typeof supplyTypesProjectStateSnapshotSchema> {
return {
...snapshot,
schemaVersion: supplyTypesProjectStateSnapshotSchemaVersion,
project: {
...snapshot.project,
enabledDistributionBoardSupplyTypes: [
...defaultDistributionBoardSupplyTypes,
],
},
};
}
function upgradeSupplyTypesProjectStateSnapshot(
snapshot: z.infer<typeof supplyTypesProjectStateSnapshotSchema>
): z.infer<typeof voltageProjectStateSnapshotSchema> {
const settings = snapshot.project;
const sectionById = new Map(
snapshot.circuitSections.map((section) => [section.id, section])
);
return {
...snapshot,
schemaVersion: voltageProjectStateSnapshotSchemaVersion,
projectDevices: snapshot.projectDevices.map((device) => ({
...device,
voltageV: resolveProjectVoltage(device.phaseType, settings),
})),
circuits: snapshot.circuits.map((circuit) => {
const section = sectionById.get(circuit.sectionId);
if (!section) {
return circuit;
}
const phaseType = resolveCircuitPhaseType(
section.key,
circuit.deviceRows.map((row) => row.phaseType)
);
return {
...circuit,
voltage: resolveProjectVoltage(phaseType, settings),
};
}),
};
}
function upgradeVoltageProjectStateSnapshot(
snapshot: z.infer<typeof voltageProjectStateSnapshotSchema>
): z.infer<typeof simultaneityFactorProjectStateSnapshotSchema> {
return {
...snapshot,
schemaVersion: simultaneityFactorProjectStateSnapshotSchemaVersion,
distributionBoards: snapshot.distributionBoards.map((board) => ({
...board,
simultaneityFactor: 1,
})),
};
}
function upgradeSimultaneityFactorProjectStateSnapshot(
snapshot: z.infer<typeof simultaneityFactorProjectStateSnapshotSchema>
): ProjectStateSnapshot {
return {
...snapshot,
schemaVersion: projectStateSnapshotSchemaVersion,
circuitSections: snapshot.circuitSections.map((section) => {
const category = initialCategoryBySectionKey(section.key);
return {
...section,
category,
groupNumber: category === null ? null : 1,
};
}),
distributionBoardComponents: [],
circuitProtectionDevices: [],
distributionBoardComponentProtectionDevices: [],
};
}
function initialCategoryBySectionKey(
key: string
): CircuitGroupCategory | null {
if (
key === "lighting" ||
key === "single_phase" ||
key === "three_phase"
) {
return key;
}
return null;
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
export function serializeProjectStateSnapshot(
snapshot: ProjectStateSnapshot
): string {
@@ -156,7 +156,6 @@ export function remapProjectState(
row.linkedProjectDeviceId === null
? null
: requiredId(deviceIds, row.linkedProjectDeviceId),
legacyConsumerId: null,
roomId:
row.roomId === null ? null : requiredId(roomIds, row.roomId),
})),
@@ -1,143 +0,0 @@
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,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;