Add protection domain foundation
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
formatGroupedCircuitIdentifier,
|
||||
formatGroupRcdIdentifier,
|
||||
formatGroupUpstreamProtectionIdentifier,
|
||||
getNextCircuitGroupNumber,
|
||||
getNextGroupedCircuitIdentifier,
|
||||
parseGroupedEquipmentIdentifier,
|
||||
} from "../src/domain/services/circuit-group-numbering.js";
|
||||
|
||||
describe("circuit group numbering", () => {
|
||||
it("formats the agreed identifiers including the leading hyphen", () => {
|
||||
assert.equal(
|
||||
formatGroupUpstreamProtectionIdentifier("lighting", 1),
|
||||
"-1F1.0"
|
||||
);
|
||||
assert.equal(formatGroupRcdIdentifier("lighting", 1), "-1Q1.0");
|
||||
assert.equal(
|
||||
formatGroupedCircuitIdentifier("lighting", 1, 2),
|
||||
"-1F1.2"
|
||||
);
|
||||
assert.equal(
|
||||
formatGroupedCircuitIdentifier("single_phase", 3, 7),
|
||||
"-2F3.7"
|
||||
);
|
||||
assert.equal(
|
||||
formatGroupedCircuitIdentifier("three_phase", 2, 1),
|
||||
"-3F2.1"
|
||||
);
|
||||
});
|
||||
|
||||
it("parses circuit and group component identifiers", () => {
|
||||
assert.deepEqual(parseGroupedEquipmentIdentifier("-1F2.4"), {
|
||||
category: "lighting",
|
||||
groupNumber: 2,
|
||||
kind: "circuit",
|
||||
circuitNumber: 4,
|
||||
});
|
||||
assert.deepEqual(parseGroupedEquipmentIdentifier("-2F3.0"), {
|
||||
category: "single_phase",
|
||||
groupNumber: 3,
|
||||
kind: "group_upstream_protection",
|
||||
circuitNumber: null,
|
||||
});
|
||||
assert.deepEqual(parseGroupedEquipmentIdentifier("-3Q1.0"), {
|
||||
category: "three_phase",
|
||||
groupNumber: 1,
|
||||
kind: "group_rcd",
|
||||
circuitNumber: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects legacy, malformed and unsupported identifiers", () => {
|
||||
for (const identifier of [
|
||||
"-1F1",
|
||||
"1F1.1",
|
||||
"-1Q1.2",
|
||||
"-4F1.1",
|
||||
"-1F0.1",
|
||||
"-1F1.01",
|
||||
]) {
|
||||
assert.equal(parseGroupedEquipmentIdentifier(identifier), null);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the highest target-group suffix plus one without filling gaps", () => {
|
||||
assert.equal(
|
||||
getNextGroupedCircuitIdentifier("lighting", 2, [
|
||||
"-1F2.1",
|
||||
"-1F2.3",
|
||||
"-1F2.7",
|
||||
"-1F1.12",
|
||||
"-2F2.9",
|
||||
"-1F2.0",
|
||||
"-1Q2.0",
|
||||
"-1F8",
|
||||
]),
|
||||
"-1F2.8"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the highest group number in the selected category plus one", () => {
|
||||
assert.equal(
|
||||
getNextCircuitGroupNumber("lighting", [
|
||||
{ category: "lighting", groupNumber: 1 },
|
||||
{ category: "lighting", groupNumber: 4 },
|
||||
{ category: "single_phase", groupNumber: 9 },
|
||||
]),
|
||||
5
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-positive group and circuit numbers", () => {
|
||||
assert.throws(
|
||||
() => formatGroupedCircuitIdentifier("lighting", 0, 1),
|
||||
RangeError
|
||||
);
|
||||
assert.throws(
|
||||
() => formatGroupedCircuitIdentifier("lighting", 1, 0),
|
||||
RangeError
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
defaultMainSwitchComponent,
|
||||
defaultSurgeProtectiveDeviceComponent,
|
||||
distributionBoardComponentPlacements,
|
||||
distributionBoardComponentRoles,
|
||||
} from "../src/shared/constants/distribution-board-component.js";
|
||||
import { circuitGroupCategories } from "../src/shared/constants/circuit-group.js";
|
||||
|
||||
describe("distribution board component catalog", () => {
|
||||
it("defines the agreed component roles and placement zones", () => {
|
||||
assert.deepEqual(distributionBoardComponentRoles, [
|
||||
"main_switch",
|
||||
"surge_protective_device",
|
||||
"group_upstream_protection",
|
||||
"group_residual_current_protection",
|
||||
"auxiliary",
|
||||
]);
|
||||
assert.deepEqual(distributionBoardComponentPlacements, [
|
||||
"header",
|
||||
"group",
|
||||
"footer",
|
||||
]);
|
||||
});
|
||||
|
||||
it("defines exactly the three supported circuit group categories", () => {
|
||||
assert.deepEqual(circuitGroupCategories, [
|
||||
"lighting",
|
||||
"single_phase",
|
||||
"three_phase",
|
||||
]);
|
||||
});
|
||||
|
||||
it("provides the fixed header component defaults", () => {
|
||||
assert.deepEqual(defaultMainSwitchComponent, {
|
||||
equipmentIdentifier: "-Q0",
|
||||
name: "Hauptschalter",
|
||||
role: "main_switch",
|
||||
placement: "header",
|
||||
});
|
||||
assert.deepEqual(defaultSurgeProtectiveDeviceComponent, {
|
||||
equipmentIdentifier: "-FA",
|
||||
name: "Überspannungsableiter",
|
||||
role: "surge_protective_device",
|
||||
placement: "header",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
allowedRatedCurrentsAByProtectionDeviceType,
|
||||
breakerTripCharacteristics,
|
||||
fuseUtilizationCategories,
|
||||
protectionDeviceTypes,
|
||||
ratedResidualCurrentsMa,
|
||||
rcdTypes,
|
||||
} from "../src/shared/constants/protection-device.js";
|
||||
import {
|
||||
createDefaultCircuitProtection,
|
||||
createDefaultGroupRcd,
|
||||
} from "../src/domain/services/protection-device-defaults.js";
|
||||
import { protectionDeviceConfigurationSchema } from "../src/shared/validation/protection-device.schemas.js";
|
||||
|
||||
describe("protection device catalog", () => {
|
||||
it("contains exactly the agreed protection device types", () => {
|
||||
assert.deepEqual(protectionDeviceTypes, [
|
||||
"D02",
|
||||
"NH000",
|
||||
"NH00",
|
||||
"NH0",
|
||||
"NH1",
|
||||
"NH2",
|
||||
"NH3",
|
||||
"LS",
|
||||
"FI",
|
||||
"FI_LS",
|
||||
"AFDD",
|
||||
]);
|
||||
assert.equal("D01" in allowedRatedCurrentsAByProtectionDeviceType, false);
|
||||
assert.equal("NH4" in allowedRatedCurrentsAByProtectionDeviceType, false);
|
||||
});
|
||||
|
||||
it("exposes the agreed device-specific rated currents", () => {
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.D02, [
|
||||
20, 25, 32, 35, 40, 50, 63,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH000, [
|
||||
6, 10, 16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160,
|
||||
]);
|
||||
assert.deepEqual(
|
||||
allowedRatedCurrentsAByProtectionDeviceType.NH00,
|
||||
allowedRatedCurrentsAByProtectionDeviceType.NH000
|
||||
);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH0, [
|
||||
6, 10, 16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224,
|
||||
250,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH1, [
|
||||
16, 20, 25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224, 250,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH2, [
|
||||
25, 32, 35, 40, 50, 63, 80, 100, 125, 160, 200, 224, 250, 300, 315,
|
||||
355, 400,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.NH3, [
|
||||
50, 63, 80, 100, 125, 160, 200, 224, 250, 315, 355, 400, 500, 630,
|
||||
800,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.FI, [
|
||||
16, 25, 40, 63, 80, 100, 125,
|
||||
]);
|
||||
assert.deepEqual(allowedRatedCurrentsAByProtectionDeviceType.LS, [
|
||||
6, 10, 13, 16, 20, 25, 32, 40, 50, 63, 80, 100, 125,
|
||||
]);
|
||||
assert.deepEqual(
|
||||
allowedRatedCurrentsAByProtectionDeviceType.FI_LS,
|
||||
allowedRatedCurrentsAByProtectionDeviceType.LS
|
||||
);
|
||||
assert.deepEqual(
|
||||
allowedRatedCurrentsAByProtectionDeviceType.AFDD,
|
||||
allowedRatedCurrentsAByProtectionDeviceType.LS
|
||||
);
|
||||
assert.deepEqual(fuseUtilizationCategories, ["gG", "gR"]);
|
||||
assert.deepEqual(breakerTripCharacteristics, ["B", "C", "D", "Z"]);
|
||||
assert.deepEqual(rcdTypes, ["A", "AC", "B", "B+"]);
|
||||
assert.deepEqual(ratedResidualCurrentsMa, [10, 30, 100, 300, 500]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("protection device validation", () => {
|
||||
it("accepts valid fuse, breaker, RCD, combined and AFDD configurations", () => {
|
||||
const configurations = [
|
||||
{
|
||||
type: "D02",
|
||||
ratedCurrentA: 20,
|
||||
fuseUtilizationCategory: "gG",
|
||||
},
|
||||
{
|
||||
type: "NH3",
|
||||
ratedCurrentA: 800,
|
||||
fuseUtilizationCategory: "gR",
|
||||
},
|
||||
{
|
||||
type: "LS",
|
||||
ratedCurrentA: 13,
|
||||
tripCharacteristic: "Z",
|
||||
},
|
||||
{
|
||||
type: "FI",
|
||||
ratedCurrentA: 40,
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
},
|
||||
{
|
||||
type: "FI_LS",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
},
|
||||
{
|
||||
type: "AFDD",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
},
|
||||
];
|
||||
|
||||
for (const configuration of configurations) {
|
||||
const result =
|
||||
protectionDeviceConfigurationSchema.safeParse(configuration);
|
||||
assert.equal(
|
||||
result.success,
|
||||
true,
|
||||
result.success
|
||||
? undefined
|
||||
: `${configuration.type}: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects rated currents that do not belong to the selected type", () => {
|
||||
assert.equal(
|
||||
protectionDeviceConfigurationSchema.safeParse({
|
||||
type: "D02",
|
||||
ratedCurrentA: 16,
|
||||
fuseUtilizationCategory: "gG",
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("requires and forbids fields according to device function", () => {
|
||||
assert.equal(
|
||||
protectionDeviceConfigurationSchema.safeParse({
|
||||
type: "LS",
|
||||
ratedCurrentA: 16,
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
protectionDeviceConfigurationSchema.safeParse({
|
||||
type: "FI",
|
||||
ratedCurrentA: 40,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
protectionDeviceConfigurationSchema.safeParse({
|
||||
type: "AFDD",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("protection device defaults", () => {
|
||||
it("creates the agreed circuit defaults", () => {
|
||||
assert.deepEqual(createDefaultCircuitProtection("lighting"), {
|
||||
type: "LS",
|
||||
ratedCurrentA: 10,
|
||||
tripCharacteristic: "B",
|
||||
});
|
||||
assert.deepEqual(createDefaultCircuitProtection("single_phase"), {
|
||||
type: "FI_LS",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
});
|
||||
assert.deepEqual(createDefaultCircuitProtection("three_phase"), {
|
||||
type: "FI_LS",
|
||||
ratedCurrentA: 16,
|
||||
tripCharacteristic: "B",
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a 40 A, type A, 30 mA group RCD", () => {
|
||||
assert.deepEqual(createDefaultGroupRcd(), {
|
||||
type: "FI",
|
||||
ratedCurrentA: 40,
|
||||
rcdType: "A",
|
||||
ratedResidualCurrentMa: 30,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns independent default objects", () => {
|
||||
const first = createDefaultCircuitProtection("lighting");
|
||||
const second = createDefaultCircuitProtection("lighting");
|
||||
assert.notEqual(first, second);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user