Derive device voltages from project settings

This commit is contained in:
2026-07-29 09:53:58 +02:00
parent b1a11397b3
commit 084103bf54
39 changed files with 2696 additions and 76 deletions
+29
View File
@@ -4,6 +4,10 @@ import {
calculateCircuitTotalPower,
calculateRowTotalPower,
} from "../src/domain/calculations/circuit-power-calculation.js";
import {
resolveCircuitPhaseType,
resolveProjectVoltage,
} from "../src/domain/services/project-voltage.service.js";
describe("circuit power calculation", () => {
it("calculates row and circuit totals from device rows", () => {
@@ -16,3 +20,28 @@ describe("circuit power calculation", () => {
});
});
describe("project voltage derivation", () => {
const settings = {
singlePhaseVoltageV: 240,
threePhaseVoltageV: 415,
};
it("derives device voltage exclusively from phase and project settings", () => {
assert.equal(resolveProjectVoltage("single_phase", settings), 240);
assert.equal(resolveProjectVoltage("three_phase", settings), 415);
});
it("uses section phase and handles unassigned circuits conservatively", () => {
assert.equal(resolveCircuitPhaseType("lighting", ["three_phase"]), "single_phase");
assert.equal(resolveCircuitPhaseType("single_phase", ["three_phase"]), "single_phase");
assert.equal(resolveCircuitPhaseType("three_phase", ["single_phase"]), "three_phase");
assert.equal(resolveCircuitPhaseType("unassigned", ["three_phase"]), "three_phase");
assert.equal(
resolveCircuitPhaseType("unassigned", ["three_phase", "single_phase"]),
"single_phase"
);
assert.equal(resolveCircuitPhaseType("unassigned", []), "single_phase");
assert.equal(resolveCircuitPhaseType("unassigned", [null]), "single_phase");
});
});
@@ -129,7 +129,7 @@ describe("circuit project-command repository", () => {
const circuit = getCircuit(fixture.context);
assert.equal(circuit.displayName, null);
assert.equal(circuit.protectionRatedCurrent, 16);
assert.equal(circuit.voltage, 400);
assert.equal(circuit.voltage, 230);
assert.equal(circuit.controlRequirement, "DALI");
assert.equal(circuit.isReserve, 1);
assert.equal(executed.revision.revisionNumber, 1);
@@ -138,7 +138,6 @@ describe("circuit project-command repository", () => {
changes: [
{ field: "displayName", value: "Licht Bestand" },
{ field: "protectionRatedCurrent", value: 10 },
{ field: "voltage", value: 230 },
{ field: "controlRequirement", value: "none" },
{ field: "isReserve", value: false },
],
@@ -149,7 +148,15 @@ describe("circuit project-command repository", () => {
.from(projectChangeSets)
.get();
assert.ok(changeSet);
assert.deepEqual(deserializeProjectCommand(changeSet.forwardPayloadJson), command);
assert.deepEqual(
deserializeProjectCommand(changeSet.forwardPayloadJson),
createCircuitUpdateProjectCommand("circuit-1", {
displayName: null,
protectionRatedCurrent: 16,
controlRequirement: "DALI",
isReserve: true,
})
);
assert.deepEqual(
deserializeProjectCommand(changeSet.inversePayloadJson),
executed.inverse
+14
View File
@@ -41,6 +41,20 @@ describe("project device circuit-first schema", () => {
assert.equal(result.success, false);
});
it("rejects manually supplied device voltages", () => {
const result = createProjectDeviceSchema.safeParse({
name: "Pumpe",
displayName: "Pumpe",
phaseType: "three_phase",
quantity: 1,
powerPerUnit: 2,
simultaneityFactor: 1,
voltageV: 500,
});
assert.equal(result.success, false);
});
it("requires a project revision for create and update commands", () => {
const device = {
name: "E-Line Pro",
@@ -12,6 +12,11 @@ import { ProjectSettingsProjectCommandRepository } from "../src/db/repositories/
import { projectRevisions } from "../src/db/schema/project-revisions.js";
import { projects } from "../src/db/schema/projects.js";
import { distributionBoards } from "../src/db/schema/distribution-boards.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { projectDevices } from "../src/db/schema/project-devices.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
import { ProjectRevisionConflictError } from "../src/domain/errors/project-revision-conflict.error.js";
import { createProjectSettingsUpdateProjectCommand } from "../src/domain/models/project-settings-project-command.model.js";
import { updateProjectSettings } from "../src/frontend/utils/api.js";
@@ -78,6 +83,86 @@ function settings(
};
}
function seedDerivedVoltageRecords(context: DatabaseContext) {
context.db.insert(projectDevices).values([
{
id: "device-single",
projectId: "project-1",
name: "Leuchte",
displayName: "Leuchte",
phaseType: "single_phase",
quantity: 1,
powerPerUnit: 0.1,
simultaneityFactor: 1,
voltageV: 230,
},
{
id: "device-three",
projectId: "project-1",
name: "Pumpe",
displayName: "Pumpe",
phaseType: "three_phase",
quantity: 1,
powerPerUnit: 2,
simultaneityFactor: 1,
voltageV: 400,
},
]).run();
const board = new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
const list = context.db
.select()
.from(circuitLists)
.where(eq(circuitLists.distributionBoardId, board.id))
.get();
assert.ok(list);
const sections = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.circuitListId, list.id))
.all();
const singleSection = sections.find((section) => section.key === "single_phase");
const threeSection = sections.find((section) => section.key === "three_phase");
assert.ok(singleSection);
assert.ok(threeSection);
context.db.insert(circuits).values([
{
id: "circuit-single",
circuitListId: list.id,
sectionId: singleSection.id,
equipmentIdentifier: "-2F1",
sortOrder: 10,
voltage: 230,
isReserve: 1,
},
{
id: "circuit-three",
circuitListId: list.id,
sectionId: threeSection.id,
equipmentIdentifier: "-3F1",
sortOrder: 10,
voltage: 400,
isReserve: 1,
},
]).run();
}
function getDerivedVoltages(context: DatabaseContext) {
return {
devices: context.db
.select({ id: projectDevices.id, voltageV: projectDevices.voltageV })
.from(projectDevices)
.all()
.sort((left, right) => left.id.localeCompare(right.id)),
circuits: context.db
.select({ id: circuits.id, voltage: circuits.voltage })
.from(circuits)
.all()
.sort((left, right) => left.id.localeCompare(right.id)),
};
}
describe("project settings project-command repository", () => {
it("validates settings commands and sends the expected revision from the frontend", async () => {
assert.throws(
@@ -137,6 +222,7 @@ describe("project settings project-command repository", () => {
it("updates both voltages and supports persisted undo and redo", () => {
const context = createTestDatabase();
try {
seedDerivedVoltageRecords(context);
const repository = new ProjectSettingsProjectCommandRepository(
context.db
);
@@ -171,6 +257,16 @@ describe("project settings project-command repository", () => {
}),
currentRevision: 1,
});
assert.deepEqual(getDerivedVoltages(context), {
devices: [
{ id: "device-single", voltageV: 240 },
{ id: "device-three", voltageV: 415 },
],
circuits: [
{ id: "circuit-single", voltage: 240 },
{ id: "circuit-three", voltage: 415 },
],
});
assert.deepEqual(forward.inverse.payload, {
name: "Testprojekt",
internalProjectNumber: "INT-1",
@@ -216,6 +312,16 @@ describe("project settings project-command repository", () => {
],
currentRevision: 2,
});
assert.deepEqual(getDerivedVoltages(context), {
devices: [
{ id: "device-single", voltageV: 230 },
{ id: "device-three", voltageV: 400 },
],
circuits: [
{ id: "circuit-single", voltage: 230 },
{ id: "circuit-three", voltage: 400 },
],
});
const redoStep = history.getNextCommand("project-1", "redo");
assert.ok(redoStep);
@@ -244,6 +350,16 @@ describe("project settings project-command repository", () => {
],
currentRevision: 3,
});
assert.deepEqual(getDerivedVoltages(context), {
devices: [
{ id: "device-single", voltageV: 240 },
{ id: "device-three", voltageV: 415 },
],
circuits: [
{ id: "circuit-single", voltage: 240 },
{ id: "circuit-three", voltage: 415 },
],
});
} finally {
context.close();
}
@@ -101,6 +101,7 @@ function createTestDatabase(): DatabaseContext {
quantity: 1,
powerPerUnit: 2.5,
simultaneityFactor: 0.8,
voltageV: 400,
})
.run();
context.db
@@ -112,6 +113,7 @@ function createTestDatabase(): DatabaseContext {
equipmentIdentifier: "-1F1",
displayName: "Ausgang",
sortOrder: 10,
voltage: 230,
isReserve: 0,
})
.run();
@@ -165,7 +167,10 @@ function changeCompleteProjectState(context: DatabaseContext) {
.run();
context.db
.update(circuits)
.set({ displayName: "Geänderter Stromkreis" })
.set({
displayName: "Geänderter Stromkreis",
voltage: 240,
})
.where(eq(circuits.id, "circuit-1"))
.run();
context.db
@@ -198,8 +203,14 @@ function changeCompleteProjectState(context: DatabaseContext) {
quantity: 2,
powerPerUnit: 0.05,
simultaneityFactor: 1,
voltageV: 240,
})
.run();
context.db
.update(projectDevices)
.set({ voltageV: 415 })
.where(eq(projectDevices.id, "device-1"))
.run();
new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "UV-02");
+30
View File
@@ -128,6 +128,36 @@ describe("project state snapshot model", () => {
]);
});
it("normalizes version-four device voltages to project settings", () => {
const previous = {
...minimalSnapshot(),
schemaVersion: 4 as const,
projectDevices: [
{
id: "device-1",
projectId: "project-1",
name: "Pumpe",
displayName: "Pumpe",
phaseType: "three_phase" as const,
connectionKind: null,
costGroup: null,
category: null,
quantity: 1,
powerPerUnit: 2,
simultaneityFactor: 1,
cosPhi: null,
remark: null,
voltageV: 500,
},
],
};
const snapshot = parseProjectStateSnapshot(previous);
assert.equal(snapshot.schemaVersion, projectStateSnapshotSchemaVersion);
assert.equal(snapshot.projectDevices[0]?.voltageV, 400);
});
it("rejects duplicate ids and cross-project ownership", () => {
assert.throws(
() =>
+1 -1
View File
@@ -156,11 +156,11 @@ describe("project device modal presentation", () => {
"Anzahl",
"Leistung je Stück [kW]",
"Gleichzeitigkeitsfaktor",
"Spannung [V]",
"Bemerkung",
]) {
assert.match(markup, new RegExp(label.replace("[", "\\[").replace("]", "\\]")));
}
assert.doesNotMatch(markup, /Spannung \[V\]/);
});
});