import crypto from "node:crypto"; import { circuitGroupCategories, type CircuitGroupCategory, } from "../../shared/constants/circuit-group.js"; import { distributionBoardComponentPlacements, distributionBoardComponentRoles, type DistributionBoardComponentPlacement, type DistributionBoardComponentRole, } from "../../shared/constants/distribution-board-component.js"; import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.js"; import type { BreakerTripCharacteristic, FuseUtilizationCategory, ProtectionDeviceType, RcdType, } from "../../shared/constants/protection-device.js"; import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js"; import { assertCircuitInsertProjectCommand, circuitInsertCommandType, circuitStructureCommandSchemaVersion, type CircuitSnapshot, } from "./circuit-structure-project-command.model.js"; export interface DistributionBoardSubtreeSectionSnapshot { id: string; circuitListId: string; key: string; displayName: string; prefix: string; sortOrder: number; category: CircuitGroupCategory | null; groupNumber: number | null; } export interface DistributionBoardSubtreeComponentProtectionSnapshot { componentId: string; type: ProtectionDeviceType; ratedCurrentA: number; fuseUtilizationCategory: FuseUtilizationCategory | null; tripCharacteristic: BreakerTripCharacteristic | null; rcdType: RcdType | null; ratedResidualCurrentMa: number | null; } export interface DistributionBoardSubtreeComponentSnapshot { component: { id: string; circuitListId: string; sectionId: string | null; equipmentIdentifier: string; name: string; role: DistributionBoardComponentRole; placement: DistributionBoardComponentPlacement; sortOrder: number; }; protectionDevice: | DistributionBoardSubtreeComponentProtectionSnapshot | null; } export interface DistributionBoardSubtreeSnapshot { distributionBoard: { id: string; projectId: string; name: string; floorId: string | null; supplyType: DistributionBoardSupplyType | null; simultaneityFactor: number; }; circuitList: { id: string; projectId: string; distributionBoardId: string; name: string; }; sections: DistributionBoardSubtreeSectionSnapshot[]; components: DistributionBoardSubtreeComponentSnapshot[]; circuits: CircuitSnapshot[]; } export function cloneDistributionBoardSubtree( source: DistributionBoardSubtreeSnapshot, name: string, createId: () => string = () => crypto.randomUUID() ): DistributionBoardSubtreeSnapshot { assertDistributionBoardSubtreeSnapshot(source); const normalizedName = name.trim(); assertNonEmptyString(normalizedName, "name"); const distributionBoardId = createId(); const circuitListId = distributionBoardId; const sectionIds = new Map( source.sections.map((section) => [section.id, createId()]) ); const circuitIds = new Map( source.circuits.map((circuit) => [circuit.id, createId()]) ); const componentIds = new Map( source.components.map(({ component }) => [component.id, createId()]) ); const clone: DistributionBoardSubtreeSnapshot = { distributionBoard: { ...source.distributionBoard, id: distributionBoardId, name: normalizedName, }, circuitList: { ...source.circuitList, id: circuitListId, distributionBoardId, name: `${normalizedName} Stromkreisliste`, }, sections: source.sections.map((section) => ({ ...section, id: requiredMappedId(sectionIds, section.id), circuitListId, })), components: source.components.map( ({ component, protectionDevice }) => { const componentId = requiredMappedId( componentIds, component.id ); return { component: { ...component, id: componentId, circuitListId, sectionId: component.sectionId === null ? null : requiredMappedId(sectionIds, component.sectionId), }, protectionDevice: protectionDevice === null ? null : { ...protectionDevice, componentId }, }; } ), circuits: source.circuits.map((circuit) => { const circuitId = requiredMappedId(circuitIds, circuit.id); return { ...circuit, id: circuitId, circuitListId, sectionId: requiredMappedId(sectionIds, circuit.sectionId), deviceRows: circuit.deviceRows.map((row) => ({ ...row, id: createId(), circuitId, })), protectionDevice: circuit.protectionDevice === null || circuit.protectionDevice === undefined ? null : { ...circuit.protectionDevice, circuitId }, }; }), }; assertDistributionBoardSubtreeSnapshot(clone); return clone; } export function assertDistributionBoardSubtreeSnapshot( value: unknown ): asserts value is DistributionBoardSubtreeSnapshot { if ( !isPlainObject(value) || Object.keys(value).length !== 5 || !isPlainObject(value.distributionBoard) || !isPlainObject(value.circuitList) || !Array.isArray(value.sections) || !Array.isArray(value.components) || !Array.isArray(value.circuits) ) { throw new Error("Distribution-board subtree snapshot is invalid."); } const board = value.distributionBoard; const list = value.circuitList; for (const field of ["id", "projectId", "name"] as const) { assertNonEmptyString(board[field], `distributionBoard.${field}`); } assertNullableString(board.floorId, "distributionBoard.floorId"); if ( board.supplyType !== null && !distributionBoardSupplyTypes.includes( board.supplyType as DistributionBoardSupplyType ) ) { throw new Error("Distribution-board supply type is invalid."); } assertFiniteNumber( board.simultaneityFactor, "distributionBoard.simultaneityFactor" ); if (board.simultaneityFactor < 0 || board.simultaneityFactor > 1) { throw new Error("Distribution-board simultaneity factor is invalid."); } for (const field of [ "id", "projectId", "distributionBoardId", "name", ] as const) { assertNonEmptyString(list[field], `circuitList.${field}`); } const circuitListId = list.id; assertNonEmptyString(circuitListId, "circuitList.id"); if ( list.projectId !== board.projectId || list.distributionBoardId !== board.id ) { throw new Error("Distribution-board circuit list ownership is invalid."); } const sectionIds = new Set(); const sectionKeys = new Set(); const sectionPrefixes = new Set(); const categoryGroups = new Set(); for (const section of value.sections) { assertSection(section, circuitListId); registerUnique(sectionIds, section.id, "circuit-section id"); registerUnique(sectionKeys, section.key, "circuit-section key"); registerUnique(sectionPrefixes, section.prefix, "circuit-section prefix"); if (section.category !== null && section.groupNumber !== null) { registerUnique( categoryGroups, `${section.category}:${section.groupNumber}`, "circuit-group number" ); } } const equipmentIdentifiers = new Set(); const componentIds = new Set(); const fixedRoles = new Set(); const groupRoles = new Set(); for (const entry of value.components) { assertComponent(entry, circuitListId, sectionIds); const component = entry.component; registerUnique(componentIds, component.id, "component id"); registerEquipmentIdentifier( equipmentIdentifiers, component.equipmentIdentifier ); if ( component.role === "main_switch" || component.role === "surge_protective_device" ) { registerUnique(fixedRoles, component.role, "fixed component role"); } if ( component.role === "group_upstream_protection" || component.role === "group_residual_current_protection" ) { registerUnique( groupRoles, `${component.sectionId}:${component.role}`, "group component role" ); } } const circuitIds = new Set(); const deviceRowIds = new Set(); for (const circuit of value.circuits) { assertCircuitInsertProjectCommand({ schemaVersion: circuitStructureCommandSchemaVersion, type: circuitInsertCommandType, payload: { circuit }, }); if ( circuit.circuitListId !== circuitListId || !sectionIds.has(circuit.sectionId) || !("protectionDevice" in circuit) ) { throw new Error("Distribution-board circuit ownership is invalid."); } registerUnique(circuitIds, circuit.id, "circuit id"); registerEquipmentIdentifier( equipmentIdentifiers, circuit.equipmentIdentifier ); for (const row of circuit.deviceRows) { registerUnique(deviceRowIds, row.id, "device-row id"); } } } function assertSection( value: unknown, circuitListId: string ): asserts value is DistributionBoardSubtreeSectionSnapshot { if (!isPlainObject(value) || Object.keys(value).length !== 8) { throw new Error("Distribution-board circuit section is invalid."); } for (const field of ["id", "circuitListId", "key", "displayName", "prefix"] as const) { assertNonEmptyString(value[field], `section.${field}`); } assertFiniteNumber(value.sortOrder, "section.sortOrder"); if (value.circuitListId !== circuitListId) { throw new Error("Distribution-board section belongs to another list."); } if ( value.category === null ? value.groupNumber !== null : !circuitGroupCategories.includes( value.category as CircuitGroupCategory ) || !Number.isInteger(value.groupNumber) || (value.groupNumber as number) <= 0 ) { throw new Error("Distribution-board circuit group is invalid."); } } function assertComponent( value: unknown, circuitListId: string, sectionIds: ReadonlySet ): asserts value is DistributionBoardSubtreeComponentSnapshot { if ( !isPlainObject(value) || Object.keys(value).length !== 2 || !isPlainObject(value.component) ) { throw new Error("Distribution-board component snapshot is invalid."); } const component = value.component; for (const field of ["id", "circuitListId", "equipmentIdentifier", "name"] as const) { assertNonEmptyString(component[field], `component.${field}`); } if ( component.circuitListId !== circuitListId || !distributionBoardComponentRoles.includes( component.role as DistributionBoardComponentRole ) || !distributionBoardComponentPlacements.includes( component.placement as DistributionBoardComponentPlacement ) ) { throw new Error("Distribution-board component values are invalid."); } assertFiniteNumber(component.sortOrder, "component.sortOrder"); assertNullableString(component.sectionId, "component.sectionId"); const fixed = component.role === "main_switch" || component.role === "surge_protective_device"; const grouped = component.role === "group_upstream_protection" || component.role === "group_residual_current_protection"; if ( (fixed && (component.placement !== "header" || component.sectionId !== null)) || (grouped && (component.placement !== "group" || typeof component.sectionId !== "string" || !sectionIds.has(component.sectionId))) || (component.role === "auxiliary" && (component.placement !== "footer" || component.sectionId !== null)) ) { throw new Error("Distribution-board component placement is invalid."); } if (value.protectionDevice === null) { if (grouped) { throw new Error("Group protection component lacks protection data."); } return; } if (!grouped || !isPlainObject(value.protectionDevice)) { throw new Error("Component protection data is invalid."); } const protection = value.protectionDevice; if ( protection.componentId !== component.id || !protectionDeviceConfigurationSchema.safeParse({ type: protection.type, ratedCurrentA: protection.ratedCurrentA, ...(protection.fuseUtilizationCategory === null ? {} : { fuseUtilizationCategory: protection.fuseUtilizationCategory }), ...(protection.tripCharacteristic === null ? {} : { tripCharacteristic: protection.tripCharacteristic }), ...(protection.rcdType === null ? {} : { rcdType: protection.rcdType }), ...(protection.ratedResidualCurrentMa === null ? {} : { ratedResidualCurrentMa: protection.ratedResidualCurrentMa }), }).success ) { throw new Error("Component protection configuration is invalid."); } } function requiredMappedId( ids: ReadonlyMap, sourceId: string ) { const id = ids.get(sourceId); if (!id) { throw new Error("Distribution-board copy reference is invalid."); } return id; } function registerEquipmentIdentifier( identifiers: Set, value: string ) { registerUnique( identifiers, value.trim().toLocaleLowerCase("de"), "equipment identifier" ); } function registerUnique( values: Set, value: string, label: string ) { if (values.has(value)) { throw new Error(`Distribution-board subtree contains duplicate ${label}.`); } values.add(value); } function assertNonEmptyString( value: unknown, field: string ): asserts value is string { if (typeof value !== "string" || !value.trim()) { throw new Error(`${field} must be a non-empty string.`); } } function assertNullableString( value: unknown, field: string ): asserts value is string | null { if (value !== null) { assertNonEmptyString(value, field); } } function assertFiniteNumber( value: unknown, field: string ): asserts value is number { if (typeof value !== "number" || !Number.isFinite(value)) { throw new Error(`${field} must be finite.`); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }