leistungsbilanz-ts/src/domain/models/circuit-protection-project-command.model.ts
Julian Appel 38019fe087 Add explicit "no protection device" state for circuits
Circuits can now record that they deliberately have no protection device
(type NONE, shown as "–"), separate from circuits whose protection is
still open. Migration 0017 makes rated_current_a nullable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:11:28 +01:00

117 lines
4.1 KiB
TypeScript

import { isValidCircuitProtectionRecord } from "../../shared/validation/protection-device.schemas.js";
import type {
BreakerTripCharacteristic,
CircuitProtectionDeviceType,
FuseUtilizationCategory,
RcdType,
} from "../../shared/constants/protection-device.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitProtectionUpdateCommandType =
"circuit-protection.update" as const;
export const circuitProtectionUpdateCommandSchemaVersion = 1 as const;
// type "NONE" records an explicit "no protection device" decision; all
// values are then null. A missing snapshot (null) means "not yet planned".
export interface CircuitProtectionSnapshot {
circuitId: string;
type: CircuitProtectionDeviceType;
ratedCurrentA: number | null;
fuseUtilizationCategory: FuseUtilizationCategory | null;
tripCharacteristic: BreakerTripCharacteristic | null;
rcdType: RcdType | null;
ratedResidualCurrentMa: number | null;
}
interface CircuitProtectionUpdatePayload {
circuitId: string;
expected: CircuitProtectionSnapshot | null;
target: CircuitProtectionSnapshot | null;
}
export interface CircuitProtectionUpdateProjectCommand
extends SerializedProjectCommand<CircuitProtectionUpdatePayload> {
schemaVersion: typeof circuitProtectionUpdateCommandSchemaVersion;
type: typeof circuitProtectionUpdateCommandType;
}
export function createCircuitProtectionUpdateProjectCommand(
circuitId: string,
expected: CircuitProtectionSnapshot | null,
target: CircuitProtectionSnapshot | null
): CircuitProtectionUpdateProjectCommand {
const command: CircuitProtectionUpdateProjectCommand = {
schemaVersion: circuitProtectionUpdateCommandSchemaVersion,
type: circuitProtectionUpdateCommandType,
payload: { circuitId, expected, target },
};
assertCircuitProtectionUpdateProjectCommand(command);
return command;
}
export function assertCircuitProtectionUpdateProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitProtectionUpdateProjectCommand {
if (
command.schemaVersion !== circuitProtectionUpdateCommandSchemaVersion ||
command.type !== circuitProtectionUpdateCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 3
) {
throw new Error("Unsupported circuit-protection update command.");
}
const { circuitId, expected, target } = command.payload;
if (typeof circuitId !== "string" || !circuitId.trim()) {
throw new Error("Circuit protection requires a circuit id.");
}
if (expected === null && target === null) {
throw new Error("Circuit protection update must change state.");
}
if (expected !== null) {
assertCircuitProtectionSnapshot(expected, circuitId);
}
if (target !== null) {
assertCircuitProtectionSnapshot(target, circuitId);
}
if (circuitProtectionSnapshotsEqual(expected, target)) {
throw new Error("Circuit protection update must change state.");
}
}
function circuitProtectionSnapshotsEqual(
left: CircuitProtectionSnapshot | null,
right: CircuitProtectionSnapshot | null
): boolean {
if (left === null || right === null) {
return left === right;
}
return (
left.circuitId === right.circuitId &&
left.type === right.type &&
left.ratedCurrentA === right.ratedCurrentA &&
left.fuseUtilizationCategory === right.fuseUtilizationCategory &&
left.tripCharacteristic === right.tripCharacteristic &&
left.rcdType === right.rcdType &&
left.ratedResidualCurrentMa === right.ratedResidualCurrentMa
);
}
export function assertCircuitProtectionSnapshot(
value: unknown,
circuitId: string
): asserts value is CircuitProtectionSnapshot {
if (
!isPlainObject(value) ||
Object.keys(value).length !== 7 ||
value.circuitId !== circuitId
) {
throw new Error("Circuit-protection snapshot is invalid.");
}
if (!isValidCircuitProtectionRecord(value)) {
throw new Error("Circuit-protection configuration is invalid.");
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}