import { distributionBoardComponentPlacements } from "../../shared/constants/distribution-board-component.js"; import type { DistributionBoardComponentPlacement } from "../../shared/constants/distribution-board-component.js"; import { protectionDeviceConfigurationSchema } from "../../shared/validation/protection-device.schemas.js"; import type { BreakerTripCharacteristic, FuseUtilizationCategory, ProtectionDeviceType, RcdType, } from "../../shared/constants/protection-device.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const distributionBoardComponentInsertCommandType = "distribution-board-component.insert" as const; export const distributionBoardComponentDeleteCommandType = "distribution-board-component.delete" as const; export const distributionBoardComponentUpdateCommandType = "distribution-board-component.update" as const; export const distributionBoardComponentStructureCommandSchemaVersion = 1 as const; type MutableDistributionBoardComponentRole = | "group_upstream_protection" | "group_residual_current_protection" | "auxiliary"; export interface DistributionBoardComponentSnapshot { component: { id: string; circuitListId: string; sectionId: string | null; equipmentIdentifier: string; name: string; role: MutableDistributionBoardComponentRole; placement: DistributionBoardComponentPlacement; sortOrder: number; }; protectionDevice: { componentId: string; type: ProtectionDeviceType; ratedCurrentA: number; fuseUtilizationCategory: FuseUtilizationCategory | null; tripCharacteristic: BreakerTripCharacteristic | null; rcdType: RcdType | null; ratedResidualCurrentMa: number | null; } | null; } interface DistributionBoardComponentStructurePayload { snapshot: DistributionBoardComponentSnapshot; } interface DistributionBoardComponentUpdatePayload { expected: DistributionBoardComponentSnapshot; target: DistributionBoardComponentSnapshot; } export interface DistributionBoardComponentInsertProjectCommand extends SerializedProjectCommand { schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion; type: typeof distributionBoardComponentInsertCommandType; } export interface DistributionBoardComponentDeleteProjectCommand extends SerializedProjectCommand { schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion; type: typeof distributionBoardComponentDeleteCommandType; } export interface DistributionBoardComponentUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof distributionBoardComponentStructureCommandSchemaVersion; type: typeof distributionBoardComponentUpdateCommandType; } export type DistributionBoardComponentStructureProjectCommand = | DistributionBoardComponentInsertProjectCommand | DistributionBoardComponentDeleteProjectCommand | DistributionBoardComponentUpdateProjectCommand; export function createDistributionBoardComponentInsertProjectCommand( snapshot: DistributionBoardComponentSnapshot ): DistributionBoardComponentInsertProjectCommand { const command: DistributionBoardComponentInsertProjectCommand = { schemaVersion: distributionBoardComponentStructureCommandSchemaVersion, type: distributionBoardComponentInsertCommandType, payload: { snapshot }, }; assertDistributionBoardComponentInsertProjectCommand(command); return command; } export function createDistributionBoardComponentDeleteProjectCommand( snapshot: DistributionBoardComponentSnapshot ): DistributionBoardComponentDeleteProjectCommand { const command: DistributionBoardComponentDeleteProjectCommand = { schemaVersion: distributionBoardComponentStructureCommandSchemaVersion, type: distributionBoardComponentDeleteCommandType, payload: { snapshot }, }; assertDistributionBoardComponentDeleteProjectCommand(command); return command; } export function createDistributionBoardComponentUpdateProjectCommand( expected: DistributionBoardComponentSnapshot, target: DistributionBoardComponentSnapshot ): DistributionBoardComponentUpdateProjectCommand { const command: DistributionBoardComponentUpdateProjectCommand = { schemaVersion: distributionBoardComponentStructureCommandSchemaVersion, type: distributionBoardComponentUpdateCommandType, payload: { expected, target }, }; assertDistributionBoardComponentUpdateProjectCommand(command); return command; } export function assertDistributionBoardComponentInsertProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardComponentInsertProjectCommand { assertStructureCommand( command, distributionBoardComponentInsertCommandType ); } export function assertDistributionBoardComponentDeleteProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardComponentDeleteProjectCommand { assertStructureCommand( command, distributionBoardComponentDeleteCommandType ); } export function assertDistributionBoardComponentUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is DistributionBoardComponentUpdateProjectCommand { if ( command.schemaVersion !== distributionBoardComponentStructureCommandSchemaVersion || command.type !== distributionBoardComponentUpdateCommandType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 2 ) { throw new Error( "Unsupported distribution-board component update command." ); } assertDistributionBoardComponentSnapshot(command.payload.expected); assertDistributionBoardComponentSnapshot(command.payload.target); assertSameComponentIdentity( command.payload.expected, command.payload.target ); } function assertStructureCommand( command: SerializedProjectCommand, type: | typeof distributionBoardComponentInsertCommandType | typeof distributionBoardComponentDeleteCommandType ) { if ( command.schemaVersion !== distributionBoardComponentStructureCommandSchemaVersion || command.type !== type || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 1 ) { throw new Error( "Unsupported distribution-board component structure command." ); } assertDistributionBoardComponentSnapshot(command.payload.snapshot); } function assertSameComponentIdentity( expected: DistributionBoardComponentSnapshot, target: DistributionBoardComponentSnapshot ) { for (const field of [ "id", "circuitListId", "sectionId", "role", "placement", ] as const) { if (expected.component[field] !== target.component[field]) { throw new Error( "Distribution-board component updates cannot change ownership or role." ); } } } export function assertDistributionBoardComponentSnapshot( value: unknown ): asserts value is DistributionBoardComponentSnapshot { if ( !isPlainObject(value) || Object.keys(value).length !== 2 || !isPlainObject(value.component) ) { throw new Error( "Distribution-board component snapshot is invalid." ); } const component = value.component; if (Object.keys(component).length !== 8) { throw new Error( "Distribution-board component snapshot is incomplete." ); } for (const field of [ "id", "circuitListId", "equipmentIdentifier", "name", ] as const) { assertNonEmptyString(component[field], `component.${field}`); } if ( component.role !== "group_upstream_protection" && component.role !== "group_residual_current_protection" && component.role !== "auxiliary" ) { throw new Error( "Fixed distribution-board components cannot use component CRUD commands." ); } if ( !distributionBoardComponentPlacements.includes( component.placement as DistributionBoardComponentPlacement ) || !Number.isFinite(component.sortOrder) ) { throw new Error( "Distribution-board component placement or sort order is invalid." ); } const groupComponent = component.role !== "auxiliary"; if ( groupComponent !== (component.placement === "group" && typeof component.sectionId === "string" && Boolean(component.sectionId.trim())) ) { throw new Error( "Group protection components require group placement and a section." ); } if ( !groupComponent && (component.placement !== "footer" || component.sectionId !== null) ) { throw new Error( "Auxiliary components require footer placement without a section." ); } if (groupComponent) { if (!isPlainObject(value.protectionDevice)) { throw new Error( "Group protection components require protection data." ); } assertProtectionDevice( value.protectionDevice, component.id as string ); } else if (value.protectionDevice !== null) { throw new Error( "Auxiliary components cannot contain protection data." ); } } function assertProtectionDevice( value: Record, componentId: string ) { if ( Object.keys(value).length !== 7 || value.componentId !== componentId ) { throw new Error( "Distribution-board component protection snapshot is invalid." ); } const result = protectionDeviceConfigurationSchema.safeParse({ type: value.type, ratedCurrentA: value.ratedCurrentA, ...(value.fuseUtilizationCategory === null ? {} : { fuseUtilizationCategory: value.fuseUtilizationCategory }), ...(value.tripCharacteristic === null ? {} : { tripCharacteristic: value.tripCharacteristic }), ...(value.rcdType === null ? {} : { rcdType: value.rcdType }), ...(value.ratedResidualCurrentMa === null ? {} : { ratedResidualCurrentMa: value.ratedResidualCurrentMa }), }); if (!result.success) { throw new Error( "Distribution-board component protection configuration is invalid." ); } } function assertNonEmptyString(value: unknown, field: 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); }