import type { SerializedProjectCommand } from "./project-command.model.js"; import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board.js"; import { projectCoverFieldMaxLengths, projectCoverFields, type ProjectCoverField, } from "../../shared/constants/project-cover.js"; export const projectSettingsUpdateCommandType = "project.update-settings" as const; // Version 3 adds the print cover fields. Versions 2 (without cover fields) // and 1 (additionally without the public-building flag) stay executable; // their missing values keep the current project values. export const projectSettingsUpdateCommandSchemaVersion = 3 as const; export const withoutCoverProjectSettingsUpdateCommandSchemaVersion = 2 as const; export const baselineProjectSettingsUpdateCommandSchemaVersion = 1 as const; export interface ProjectSettingsValues extends Record { name: string; internalProjectNumber: string | null; externalProjectNumber: string | null; buildingOwner: string | null; description: string | null; isPublicBuilding: boolean; singlePhaseVoltageV: number; threePhaseVoltageV: number; enabledDistributionBoardSupplyTypes: DistributionBoardSupplyType[]; } export type WithoutCoverProjectSettingsValues = Omit< ProjectSettingsValues, ProjectCoverField >; export type BaselineProjectSettingsValues = Omit< WithoutCoverProjectSettingsValues, "isPublicBuilding" >; export interface CurrentProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof projectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } export interface WithoutCoverProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof withoutCoverProjectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } export interface BaselineProjectSettingsUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof baselineProjectSettingsUpdateCommandSchemaVersion; type: typeof projectSettingsUpdateCommandType; } export type ProjectSettingsUpdateProjectCommand = | CurrentProjectSettingsUpdateProjectCommand | WithoutCoverProjectSettingsUpdateProjectCommand | BaselineProjectSettingsUpdateProjectCommand; export function createProjectSettingsUpdateProjectCommand( values: ProjectSettingsValues ): CurrentProjectSettingsUpdateProjectCommand { const command: CurrentProjectSettingsUpdateProjectCommand = { schemaVersion: projectSettingsUpdateCommandSchemaVersion, type: projectSettingsUpdateCommandType, payload: { ...values }, }; assertProjectSettingsUpdateProjectCommand(command); return command; } export function assertProjectSettingsUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is ProjectSettingsUpdateProjectCommand { if ( command.type !== projectSettingsUpdateCommandType || (command.schemaVersion !== projectSettingsUpdateCommandSchemaVersion && command.schemaVersion !== withoutCoverProjectSettingsUpdateCommandSchemaVersion && command.schemaVersion !== baselineProjectSettingsUpdateCommandSchemaVersion) ) { throw new Error("Unsupported project settings update command."); } if (!isPlainObject(command.payload)) { throw new Error( "Project settings update command contains invalid values." ); } if ( !isTrimmedString(command.payload.name, 1, 200) || !isNullableTrimmedString(command.payload.internalProjectNumber, 100) || !isNullableTrimmedString(command.payload.externalProjectNumber, 100) || !isNullableTrimmedString(command.payload.buildingOwner, 200) || !isNullableTrimmedString(command.payload.description, 2000) || !isPositiveFiniteNumber(command.payload.singlePhaseVoltageV) || !isPositiveFiniteNumber(command.payload.threePhaseVoltageV) ) { throw new Error( "Project settings update command contains invalid values." ); } if ( !isValidSupplyTypes( command.payload.enabledDistributionBoardSupplyTypes ) ) { throw new Error( "Project settings update command contains invalid supply types." ); } const payload = command.payload; const expectedKeyCount = command.schemaVersion === baselineProjectSettingsUpdateCommandSchemaVersion ? 8 : command.schemaVersion === withoutCoverProjectSettingsUpdateCommandSchemaVersion ? 9 : 9 + projectCoverFields.length; if ( Object.keys(payload).length !== expectedKeyCount || (command.schemaVersion !== baselineProjectSettingsUpdateCommandSchemaVersion && typeof payload.isPublicBuilding !== "boolean") || (command.schemaVersion === projectSettingsUpdateCommandSchemaVersion && !projectCoverFields.every((field) => isNullableTrimmedString(payload[field], projectCoverFieldMaxLengths[field]) )) ) { throw new Error( "Project settings update command contains invalid values." ); } } function isValidSupplyTypes( value: unknown ): value is DistributionBoardSupplyType[] { return ( Array.isArray(value) && value.length > 0 && new Set(value).size === value.length && value.every( (entry) => typeof entry === "string" && distributionBoardSupplyTypes.includes( entry as DistributionBoardSupplyType ) ) ); } function isPositiveFiniteNumber(value: unknown): value is number { return ( typeof value === "number" && Number.isFinite(value) && value > 0 ); } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function isTrimmedString( value: unknown, minimumLength: number, maximumLength: number ): value is string { return ( typeof value === "string" && value === value.trim() && value.length >= minimumLength && value.length <= maximumLength ); } function isNullableTrimmedString( value: unknown, maximumLength: number ): value is string | null { return ( value === null || (isTrimmedString(value, 1, maximumLength)) ); }