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"; import "./circuit-group-structure-project-command.repository.test.js"; import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js"; import "./circuit-group-renumber-project-command.repository.test.js"; import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js"; import "./circuit-group-move-project-command.repository.test.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 ); }); }); describe("circuit group renumber planning", () => { const groups = [ { id: "lighting-1", category: "lighting" as const, groupNumber: 1, prefix: "-1F1.", circuits: [ { id: "circuit-1", equipmentIdentifier: "-1F1.1" }, { id: "circuit-2", equipmentIdentifier: "-1F1.7" }, ], components: [ { id: "fuse-1", role: "group_upstream_protection" as const, equipmentIdentifier: "-1F1.0", }, { id: "rcd-1", role: "group_residual_current_protection" as const, equipmentIdentifier: "-1Q1.0", }, ], }, { id: "lighting-2", category: "lighting" as const, groupNumber: 2, prefix: "-1F2.", circuits: [ { id: "circuit-3", equipmentIdentifier: "-1F2.4" }, ], components: [], }, { id: "single-1", category: "single_phase" as const, groupNumber: 1, prefix: "-2F1.", circuits: [], components: [], }, ]; it("preserves circuit suffixes and maps optional group components", () => { const plan = createCircuitGroupRenumberPlan(groups, [ { groupId: "lighting-1", targetGroupNumber: 3 }, ]); assert.deepEqual(plan.groups[0], { groupId: "lighting-1", category: "lighting", expectedGroupNumber: 1, targetGroupNumber: 3, expectedPrefix: "-1F1.", targetPrefix: "-1F3.", circuits: [ { circuitId: "circuit-1", expectedEquipmentIdentifier: "-1F1.1", targetEquipmentIdentifier: "-1F3.1", }, { circuitId: "circuit-2", expectedEquipmentIdentifier: "-1F1.7", targetEquipmentIdentifier: "-1F3.7", }, ], components: [ { componentId: "fuse-1", expectedEquipmentIdentifier: "-1F1.0", targetEquipmentIdentifier: "-1F3.0", }, { componentId: "rcd-1", expectedEquipmentIdentifier: "-1Q1.0", targetEquipmentIdentifier: "-1Q3.0", }, ], }); }); it("allows a number swap when every affected group participates", () => { const plan = createCircuitGroupRenumberPlan(groups, [ { groupId: "lighting-1", targetGroupNumber: 2 }, { groupId: "lighting-2", targetGroupNumber: 1 }, ]); assert.deepEqual( plan.groups.map((group) => [ group.expectedGroupNumber, group.targetGroupNumber, ]), [ [1, 2], [2, 1], ] ); }); it("rejects collisions, no-op requests and mismatched current BMKs", () => { assert.throws( () => createCircuitGroupRenumberPlan(groups, [ { groupId: "lighting-1", targetGroupNumber: 2 }, ]), /unchanged group/ ); assert.throws( () => createCircuitGroupRenumberPlan(groups, [ { groupId: "lighting-1", targetGroupNumber: 1 }, ]), /must change/ ); assert.throws( () => createCircuitGroupRenumberPlan( [ { ...groups[0], circuits: [ { id: "circuit-1", equipmentIdentifier: "-1F9.1", }, ], }, ], [{ groupId: "lighting-1", targetGroupNumber: 3 }] ), /Circuit BMK does not match/ ); }); }); describe("circuit group move planning", () => { const sourceGroup = { id: "lighting-1", category: "lighting" as const, groupNumber: 1, prefix: "-1F1.", }; const targetGroup = { id: "lighting-2", category: "lighting" as const, groupNumber: 2, prefix: "-1F2.", }; it("uses the highest target suffix plus one and stores both positions", () => { assert.deepEqual( createCircuitGroupMovePlan({ circuit: { id: "circuit-1", circuitListId: "list-1", sectionId: "lighting-1", equipmentIdentifier: "-1F1.7", sortOrder: 20, }, sourceGroup, targetGroup, targetCircuitEquipmentIdentifiers: [ "-1F2.1", "-1F2.4", "-1F2.9", ], targetSortOrder: 40, }), { circuitId: "circuit-1", circuitListId: "list-1", expectedSectionId: "lighting-1", targetSectionId: "lighting-2", expectedEquipmentIdentifier: "-1F1.7", targetEquipmentIdentifier: "-1F2.10", expectedSortOrder: 20, targetSortOrder: 40, } ); }); it("does not fill target gaps", () => { assert.equal( createCircuitGroupMovePlan({ circuit: { id: "circuit-1", circuitListId: "list-1", sectionId: "lighting-1", equipmentIdentifier: "-1F1.2", sortOrder: 10, }, sourceGroup, targetGroup, targetCircuitEquipmentIdentifiers: ["-1F2.1", "-1F2.3"], targetSortOrder: 20, }).targetEquipmentIdentifier, "-1F2.4" ); }); it("rejects cross-category, same-group and mismatched source moves", () => { const base = { circuit: { id: "circuit-1", circuitListId: "list-1", sectionId: "lighting-1", equipmentIdentifier: "-1F1.1", sortOrder: 10, }, sourceGroup, targetGroup, targetCircuitEquipmentIdentifiers: [] as string[], targetSortOrder: 10, }; assert.throws( () => createCircuitGroupMovePlan({ ...base, targetGroup: { id: "single-1", category: "single_phase", groupNumber: 1, prefix: "-2F1.", }, }), /same category/ ); assert.throws( () => createCircuitGroupMovePlan({ ...base, targetGroup: sourceGroup, }), /different groups/ ); assert.throws( () => createCircuitGroupMovePlan({ ...base, circuit: { ...base.circuit, equipmentIdentifier: "-1F9.1", }, }), /does not match/ ); }); });