import crypto from "node:crypto"; import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.js"; import { defaultMainSwitchComponent, defaultSurgeProtectiveDeviceComponent, } from "../../shared/constants/distribution-board-component.js"; import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js"; import type { DistributionBoardComponentPlacement, DistributionBoardComponentRole, } from "../../shared/constants/distribution-board-component.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardInsertCommandType = "distribution-board.insert" as const; export const distributionBoardDeleteCommandType = "distribution-board.delete" as const; export const distributionBoardStructureCommandSchemaVersion = 1 as const; export const defaultCircuitSectionDefinitions = [ { key: "lighting", displayName: "Beleuchtung 1", prefix: "-1F1.", sortOrder: 10, category: "lighting", groupNumber: 1, }, { key: "single_phase", displayName: "1-phasig 1", prefix: "-2F1.", sortOrder: 20, category: "single_phase", groupNumber: 1, }, { key: "three_phase", displayName: "3-phasig 1", prefix: "-3F1.", sortOrder: 30, category: "three_phase", groupNumber: 1, }, ] as const; export interface DistributionBoardStructureSnapshot { distributionBoard: { id: string; projectId: string; name: string; floorId: string | null; supplyType: DistributionBoardSupplyType | null; }; circuitList: { id: string; projectId: string; distributionBoardId: string; name: string; }; sections: Array<{ id: string; circuitListId: string; key: string; displayName: string; prefix: string; sortOrder: number; category: CircuitGroupCategory; groupNumber: number; }>; components: Array<{ id: string; circuitListId: string; sectionId: null; equipmentIdentifier: string; name: string; role: DistributionBoardComponentRole; placement: DistributionBoardComponentPlacement; sortOrder: number; }>; } export interface NormalizedDistributionBoardStructureSnapshot { distributionBoard: DistributionBoardStructureSnapshot["distributionBoard"]; circuitList: DistributionBoardStructureSnapshot["circuitList"]; sections: DistributionBoardStructureSnapshot["sections"]; components: DistributionBoardStructureSnapshot["components"]; } interface DistributionBoardStructureCommandPayload { structure: DistributionBoardStructureSnapshot; } interface DistributionBoardStructureProjectCommandBase extends SerializedProjectCommand { schemaVersion: typeof distributionBoardStructureCommandSchemaVersion; type: | typeof distributionBoardInsertCommandType | typeof distributionBoardDeleteCommandType; } export interface DistributionBoardInsertProjectCommand extends DistributionBoardStructureProjectCommandBase { type: typeof distributionBoardInsertCommandType; } export interface DistributionBoardDeleteProjectCommand extends DistributionBoardStructureProjectCommandBase { type: typeof distributionBoardDeleteCommandType; } export type DistributionBoardStructureProjectCommand = | DistributionBoardInsertProjectCommand | DistributionBoardDeleteProjectCommand; export function createDistributionBoardStructureSnapshot( projectId: string, name: string, input: { floorId?: string | null; supplyType?: DistributionBoardSupplyType | null; } = {} ): DistributionBoardStructureSnapshot { const normalizedName = name.trim(); assertNonEmptyString(projectId, "projectId"); assertNonEmptyString(normalizedName, "name"); const structureId = crypto.randomUUID(); const structure: DistributionBoardStructureSnapshot = { distributionBoard: { id: structureId, projectId, name: normalizedName, floorId: input.floorId ?? null, supplyType: input.supplyType ?? null, }, circuitList: { id: structureId, projectId, distributionBoardId: structureId, name: `${normalizedName} Stromkreisliste`, }, sections: defaultCircuitSectionDefinitions.map((section) => ({ id: crypto.randomUUID(), circuitListId: structureId, ...section, })), components: [ { id: crypto.randomUUID(), circuitListId: structureId, sectionId: null, ...defaultMainSwitchComponent, sortOrder: 10, }, { id: crypto.randomUUID(), circuitListId: structureId, sectionId: null, ...defaultSurgeProtectiveDeviceComponent, sortOrder: 20, }, ], }; assertDistributionBoardStructureSnapshot(structure); return structure; } export function createDistributionBoardInsertProjectCommand( structure: DistributionBoardStructureSnapshot ): DistributionBoardInsertProjectCommand { const command: DistributionBoardInsertProjectCommand = { schemaVersion: distributionBoardStructureCommandSchemaVersion, type: distributionBoardInsertCommandType, payload: { structure }, }; assertDistributionBoardInsertProjectCommand(command); return command; } export function createDistributionBoardDeleteProjectCommand( structure: DistributionBoardStructureSnapshot ): DistributionBoardDeleteProjectCommand { const command: DistributionBoardDeleteProjectCommand = { schemaVersion: distributionBoardStructureCommandSchemaVersion, type: distributionBoardDeleteCommandType, payload: { structure }, }; assertDistributionBoardDeleteProjectCommand(command); return command; } export function assertDistributionBoardInsertProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardStructureProjectCommand { assertDistributionBoardStructureProjectCommand( command, distributionBoardInsertCommandType ); } export function assertDistributionBoardDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardStructureProjectCommand { assertDistributionBoardStructureProjectCommand( command, distributionBoardDeleteCommandType ); } export function normalizeDistributionBoardStructureProjectCommand( command: DistributionBoardStructureProjectCommand ): NormalizedDistributionBoardStructureSnapshot { return command.payload.structure; } export function invertDistributionBoardStructureProjectCommand( command: DistributionBoardStructureProjectCommand ): DistributionBoardStructureProjectCommand { return { ...command, type: command.type === distributionBoardInsertCommandType ? distributionBoardDeleteCommandType : distributionBoardInsertCommandType, } as DistributionBoardStructureProjectCommand; } export function assertDistributionBoardStructureSnapshot( structure: unknown ): asserts structure is DistributionBoardStructureSnapshot { assertDistributionBoardStructureSnapshotVersion(structure); } function assertDistributionBoardStructureProjectCommand( command: SerializedProjectCommand, expectedType: | typeof distributionBoardInsertCommandType | typeof distributionBoardDeleteCommandType ) { if ( command.type !== expectedType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error("Unsupported distribution-board structure command."); } if ( command.schemaVersion !== distributionBoardStructureCommandSchemaVersion ) { throw new Error("Unsupported distribution-board structure command."); } assertDistributionBoardStructureSnapshotVersion(command.payload.structure); } function assertDistributionBoardStructureSnapshotVersion( structure: unknown ) { if ( !isPlainObject(structure) || Object.keys(structure).length !== 4 ) { throw new Error("Distribution-board structure is invalid."); } const { distributionBoard, circuitList, sections, components } = structure; const expectedSections = defaultCircuitSectionDefinitions; if ( !isPlainObject(distributionBoard) || Object.keys(distributionBoard).length !== 5 || !isPlainObject(circuitList) || Object.keys(circuitList).length !== 4 || !Array.isArray(sections) || sections.length !== expectedSections.length || !Array.isArray(components) || components.length !== 2 ) { throw new Error("Distribution-board structure is incomplete."); } for (const field of ["id", "projectId", "name"] as const) { assertNonEmptyString( distributionBoard[field], `distributionBoard.${field}` ); } assertNullableId(distributionBoard.floorId, "distributionBoard.floorId"); assertNullableSupplyType(distributionBoard.supplyType); for (const [field, value] of Object.entries(circuitList)) { assertNonEmptyString(value, `circuitList.${field}`); } if ( circuitList.projectId !== distributionBoard.projectId || circuitList.distributionBoardId !== distributionBoard.id || circuitList.name !== `${distributionBoard.name} Stromkreisliste` ) { throw new Error("Distribution board and circuit list are inconsistent."); } const sectionIds = new Set(); for (let index = 0; index < sections.length; index += 1) { const section = sections[index]; const expected = expectedSections[index]; if ( !isPlainObject(section) || Object.keys(section).length !== 8 ) { throw new Error("Default circuit section is invalid."); } assertNonEmptyString(section.id, "section.id"); if (sectionIds.has(section.id)) { throw new Error("Default circuit-section ids must be unique."); } sectionIds.add(section.id); if ( section.circuitListId !== circuitList.id || section.key !== expected.key || section.displayName !== expected.displayName || section.prefix !== expected.prefix || section.sortOrder !== expected.sortOrder || section.category !== expected.category || section.groupNumber !== expected.groupNumber ) { throw new Error( "Distribution-board structure has invalid default sections." ); } } assertDefaultComponents(components, circuitList.id); } function assertDefaultComponents( components: unknown, circuitListId: unknown ) { if (!Array.isArray(components)) { throw new Error("Default distribution-board components are invalid."); } const expectedComponents = [ { ...defaultMainSwitchComponent, sortOrder: 10 }, { ...defaultSurgeProtectiveDeviceComponent, sortOrder: 20 }, ]; const componentIds = new Set(); for (let index = 0; index < expectedComponents.length; index += 1) { const component = components[index]; const expected = expectedComponents[index]; if (!isPlainObject(component) || Object.keys(component).length !== 8) { throw new Error( "Default distribution-board component is invalid." ); } assertNonEmptyString(component.id, "component.id"); if (componentIds.has(component.id)) { throw new Error( "Default distribution-board component ids must be unique." ); } componentIds.add(component.id); if ( component.circuitListId !== circuitListId || component.sectionId !== null || component.equipmentIdentifier !== expected.equipmentIdentifier || component.name !== expected.name || component.role !== expected.role || component.placement !== expected.placement || component.sortOrder !== expected.sortOrder ) { throw new Error( "Distribution-board structure has invalid default components." ); } } } function assertNullableId(value: unknown, field: string) { if (value !== null) { assertNonEmptyString(value, field); } } function assertNullableSupplyType( value: unknown ): asserts value is DistributionBoardSupplyType | null { if ( value !== null && !distributionBoardSupplyTypes.includes( value as DistributionBoardSupplyType ) ) { throw new Error("distributionBoard.supplyType is invalid."); } } 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 isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }