Persist circuit group moves

This commit is contained in:
2026-07-30 20:03:11 +02:00
parent 9ad91887b2
commit 3399fcd88b
15 changed files with 652 additions and 4 deletions
+5
View File
@@ -321,6 +321,11 @@ Explicit group-number changes use `circuit-group.renumber`. The command carries
the complete expected/target group, circuit-BMK and optional group-component
BMK plan, applies swaps through collision-safe temporary values and preserves
circuit suffixes. Undo/Redo uses the exact inverse plan.
Cross-group circuit moves use `circuit.move-group` and are limited to distinct
groups of the same category. The stored target BMK is highest target suffix
plus one at planning time and is never recalculated for Redo. Only circuit
group, BMK and sort order change; device rows and circuit protection retain the
stable circuit id.
Distribution-board floor assignment and a project-enabled supply type use
`distribution-board.update`; both values are snapshot/export fields and one
persistent undo step.
+6 -1
View File
@@ -325,7 +325,12 @@ vollständigen Stromkreises zwischen zwei verschiedenen Gruppen derselben
Kategorie. Die Ziel-BMK verwendet die höchste vorhandene Stromkreis-Endnummer
der Zielgruppe plus eins; Lücken werden nicht gefüllt. Quell-/Zielgruppe,
Quell-/Zielposition und beide BMKs werden festgeschrieben, damit eine spätere
Wiederholung nichts neu berechnet. Die persistente Ausführung folgt separat.
Wiederholung nichts neu berechnet.
`circuit.move-group` führt den geplanten Wechsel als eine Projekt-Revision aus.
Nur `sectionId`, `equipmentIdentifier` und `sortOrder` des Stromkreises ändern
sich. Gerätezeilen und das getrennte 1:1-Schutzgerät bleiben über die stabile
Stromkreis-UUID verbunden. Der inverse Wechsel speichert Quellgruppe, BMK und
Position vollständig für dauerhaftes Undo/Redo.
`distribution-board.update` versioniert Etage, Netzart und den
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
+1 -1
View File
@@ -28,7 +28,7 @@ requirements and intended sequencing, not proof of implementation.
- [x] Phase D1a: deterministic collision-aware group-renumber plan.
- [x] Phase D1b: persistent collision-safe group-renumber command.
- [x] Phase D2a: deterministic same-category circuit-move plan.
- [ ] Phase D2b: persistent same-category circuit-move command.
- [x] Phase D2b: persistent same-category circuit-move command.
- [ ] Phase D3: confirmed populated-group deletion.
- [ ] Phase E: editor projection and editing.
- [ ] Phase F: documentation and full GUI verification.
@@ -663,8 +663,8 @@ Acceptance:
### D. Group Numbering and Circuit Moves
Status: In progress. Group renumbering D1 and deterministic circuit-move
planning D2a are complete; persistent moves D2b and populated deletion D3
remain pending.
planning and persistent moves D2 are complete; populated deletion D3 remains
pending.
- implement nested identifier generation
- support same-category cross-group circuit moves
@@ -700,6 +700,17 @@ Implemented in D2a:
and never fills gaps
- the computed target BMK is part of the plan and is not recalculated for Redo
Implemented in D2b:
- `circuit.move-group` atomically changes only the circuit's group, BMK and
sort position
- the circuit UUID remains stable, so its device rows and one-to-one protection
device remain connected without copying
- execution revalidates project/list ownership, both group identities and the
complete expected circuit state
- Undo/Redo uses the stored inverse move and late failures restore source
group, BMK and position
Acceptance:
- target identifiers use highest suffix plus one
@@ -0,0 +1,104 @@
import { and, eq } from "drizzle-orm";
import {
assertCircuitGroupMoveProjectCommand,
createCircuitGroupMoveProjectCommand,
invertCircuitGroupMovePlan,
} from "../../domain/models/circuit-group-move-project-command.model.js";
import type {
CircuitGroupMoveProjectCommandStore,
ExecuteCircuitGroupMoveCommandInput,
} from "../../domain/ports/circuit-group-move-project-command.store.js";
import { parseGroupedEquipmentIdentifier } from "../../domain/services/circuit-group-numbering.js";
import type { AppDatabase } from "../database-context.js";
import { circuitLists } from "../schema/circuit-lists.js";
import { circuitSections } from "../schema/circuit-sections.js";
import { circuits } from "../schema/circuits.js";
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
export class CircuitGroupMoveProjectCommandRepository
implements CircuitGroupMoveProjectCommandStore
{
constructor(private readonly database: AppDatabase) {}
execute(input: ExecuteCircuitGroupMoveCommandInput) {
assertCircuitGroupMoveProjectCommand(input.command);
return executeProjectCommandTransaction(
this.database,
input,
(tx) => this.applyCommand(tx, input)
);
}
private applyCommand(
database: AppDatabase,
input: ExecuteCircuitGroupMoveCommandInput
) {
const plan = input.command.payload;
const list = database
.select({ projectId: circuitLists.projectId })
.from(circuitLists)
.where(eq(circuitLists.id, plan.circuitListId))
.get();
if (!list || list.projectId !== input.projectId) {
throw new Error("Circuit list does not belong to project.");
}
const source = database
.select()
.from(circuitSections)
.where(eq(circuitSections.id, plan.expectedSectionId))
.get();
const target = database
.select()
.from(circuitSections)
.where(eq(circuitSections.id, plan.targetSectionId))
.get();
const expectedBmk = parseGroupedEquipmentIdentifier(
plan.expectedEquipmentIdentifier
);
const targetBmk = parseGroupedEquipmentIdentifier(
plan.targetEquipmentIdentifier
);
if (
!source ||
!target ||
source.circuitListId !== plan.circuitListId ||
target.circuitListId !== plan.circuitListId ||
source.category === null ||
source.category !== target.category ||
source.category !== expectedBmk?.category ||
source.groupNumber !== expectedBmk.groupNumber ||
target.category !== targetBmk?.category ||
target.groupNumber !== targetBmk.groupNumber
) {
throw new Error(
"Circuit move requires matching source and target groups."
);
}
const updated = database
.update(circuits)
.set({
sectionId: plan.targetSectionId,
equipmentIdentifier: plan.targetEquipmentIdentifier,
sortOrder: plan.targetSortOrder,
})
.where(
and(
eq(circuits.id, plan.circuitId),
eq(circuits.circuitListId, plan.circuitListId),
eq(circuits.sectionId, plan.expectedSectionId),
eq(
circuits.equipmentIdentifier,
plan.expectedEquipmentIdentifier
),
eq(circuits.sortOrder, plan.expectedSortOrder)
)
)
.run();
if (updated.changes !== 1) {
throw new Error("Circuit changed before group move.");
}
return createCircuitGroupMoveProjectCommand(
invertCircuitGroupMovePlan(plan)
);
}
}
@@ -0,0 +1,92 @@
import { parseGroupedEquipmentIdentifier } from "../services/circuit-group-numbering.js";
import type { CircuitGroupMovePlan } from "../services/circuit-group-move-planning.js";
import type { SerializedProjectCommand } from "./project-command.model.js";
export const circuitGroupMoveCommandType = "circuit.move-group" as const;
export const circuitGroupMoveCommandSchemaVersion = 1 as const;
export interface CircuitGroupMoveProjectCommand
extends SerializedProjectCommand<CircuitGroupMovePlan> {
schemaVersion: typeof circuitGroupMoveCommandSchemaVersion;
type: typeof circuitGroupMoveCommandType;
}
export function createCircuitGroupMoveProjectCommand(
plan: CircuitGroupMovePlan
): CircuitGroupMoveProjectCommand {
const command: CircuitGroupMoveProjectCommand = {
schemaVersion: circuitGroupMoveCommandSchemaVersion,
type: circuitGroupMoveCommandType,
payload: plan,
};
assertCircuitGroupMoveProjectCommand(command);
return command;
}
export function assertCircuitGroupMoveProjectCommand(
command: SerializedProjectCommand<unknown>
): asserts command is CircuitGroupMoveProjectCommand {
if (
command.schemaVersion !== circuitGroupMoveCommandSchemaVersion ||
command.type !== circuitGroupMoveCommandType ||
!isPlainObject(command.payload) ||
Object.keys(command.payload).length !== 8
) {
throw new Error("Unsupported circuit group-move command.");
}
for (const field of [
"circuitId",
"circuitListId",
"expectedSectionId",
"targetSectionId",
"expectedEquipmentIdentifier",
"targetEquipmentIdentifier",
] as const) {
if (
typeof command.payload[field] !== "string" ||
!command.payload[field].trim()
) {
throw new Error(`Circuit group-move ${field} is invalid.`);
}
}
if (
command.payload.expectedSectionId === command.payload.targetSectionId ||
!Number.isFinite(command.payload.expectedSortOrder) ||
!Number.isFinite(command.payload.targetSortOrder)
) {
throw new Error("Circuit group-move positions are invalid.");
}
const expected = parseGroupedEquipmentIdentifier(
command.payload.expectedEquipmentIdentifier as string
);
const target = parseGroupedEquipmentIdentifier(
command.payload.targetEquipmentIdentifier as string
);
if (
expected?.kind !== "circuit" ||
target?.kind !== "circuit" ||
expected.category !== target.category ||
expected.groupNumber === target.groupNumber
) {
throw new Error("Circuit group-move BMKs are incompatible.");
}
}
export function invertCircuitGroupMovePlan(
plan: CircuitGroupMovePlan
): CircuitGroupMovePlan {
return {
circuitId: plan.circuitId,
circuitListId: plan.circuitListId,
expectedSectionId: plan.targetSectionId,
targetSectionId: plan.expectedSectionId,
expectedEquipmentIdentifier: plan.targetEquipmentIdentifier,
targetEquipmentIdentifier: plan.expectedEquipmentIdentifier,
expectedSortOrder: plan.targetSortOrder,
targetSortOrder: plan.expectedSortOrder,
};
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
@@ -0,0 +1,22 @@
import type { CircuitGroupMoveProjectCommand } from "../models/circuit-group-move-project-command.model.js";
import type {
AppendedProjectRevision,
ProjectRevisionSource,
} from "./project-revision.store.js";
export interface ExecuteCircuitGroupMoveCommandInput {
projectId: string;
expectedRevision: number;
source: ProjectRevisionSource;
description?: string;
actorId?: string;
historyTargetChangeSetId?: string;
command: CircuitGroupMoveProjectCommand;
}
export interface CircuitGroupMoveProjectCommandStore {
execute(input: ExecuteCircuitGroupMoveCommandInput): {
revision: AppendedProjectRevision;
inverse: CircuitGroupMoveProjectCommand;
};
}
@@ -100,6 +100,7 @@ import type { CircuitSectionRenumberProjectCommandStore } from "../ports/circuit
import type { CircuitStructureProjectCommandStore } from "../ports/circuit-structure-project-command.store.js";
import type { CircuitGroupStructureProjectCommandStore } from "../ports/circuit-group-structure-project-command.store.js";
import type { CircuitGroupRenumberProjectCommandStore } from "../ports/circuit-group-renumber-project-command.store.js";
import type { CircuitGroupMoveProjectCommandStore } from "../ports/circuit-group-move-project-command.store.js";
import type { DistributionBoardStructureProjectCommandStore } from "../ports/distribution-board-structure-project-command.store.js";
import type { DistributionBoardComponentStructureProjectCommandStore } from "../ports/distribution-board-component-structure-project-command.store.js";
import type {
@@ -126,6 +127,10 @@ import {
assertCircuitGroupRenumberProjectCommand,
circuitGroupRenumberCommandType,
} from "../models/circuit-group-renumber-project-command.model.js";
import {
assertCircuitGroupMoveProjectCommand,
circuitGroupMoveCommandType,
} from "../models/circuit-group-move-project-command.model.js";
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
import type { ProjectDeviceProjectCommandStore } from "../ports/project-device-project-command.store.js";
import type { ProjectDeviceRowSyncProjectCommandStore } from "../ports/project-device-row-sync-project-command.store.js";
@@ -163,6 +168,7 @@ export class ProjectCommandService implements ProjectCommandExecutor {
private readonly distributionBoardComponentStructureStore: DistributionBoardComponentStructureProjectCommandStore,
private readonly circuitGroupStructureStore: CircuitGroupStructureProjectCommandStore,
private readonly circuitGroupRenumberStore: CircuitGroupRenumberProjectCommandStore,
private readonly circuitGroupMoveStore: CircuitGroupMoveProjectCommandStore,
private readonly historyStore: ProjectHistoryStore
) {}
@@ -386,6 +392,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
command: input.command,
}).revision;
}
case circuitGroupMoveCommandType: {
assertCircuitGroupMoveProjectCommand(input.command);
return this.circuitGroupMoveStore.execute({
...input,
command: input.command,
}).revision;
}
case projectFloorInsertCommandType: {
assertProjectFloorInsertProjectCommand(input.command);
return this.projectLocationStructureStore.execute({
@@ -41,6 +41,7 @@ const commandTypeLabels: Record<string, string> = {
"circuit-group.update": "Stromkreisgruppe bearbeitet",
"circuit-group.reorder": "Stromkreisgruppen sortiert",
"circuit-group.renumber": "Stromkreisgruppen neu nummeriert",
"circuit.move-group": "Stromkreis in andere Gruppe verschoben",
"project-floor.insert": "Geschoss angelegt",
"project-floor.delete": "Geschoss entfernt",
"project-room.insert": "Raum angelegt",
@@ -8,6 +8,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../../db/reposit
import { CircuitStructureProjectCommandRepository } from "../../db/repositories/circuit-structure-project-command.repository.js";
import { CircuitGroupStructureProjectCommandRepository } from "../../db/repositories/circuit-group-structure-project-command.repository.js";
import { CircuitGroupRenumberProjectCommandRepository } from "../../db/repositories/circuit-group-renumber-project-command.repository.js";
import { CircuitGroupMoveProjectCommandRepository } from "../../db/repositories/circuit-group-move-project-command.repository.js";
import { DistributionBoardStructureProjectCommandRepository } from "../../db/repositories/distribution-board-structure-project-command.repository.js";
import { DistributionBoardComponentStructureProjectCommandRepository } from "../../db/repositories/distribution-board-component-structure-project-command.repository.js";
import { ProjectHistoryRepository } from "../../db/repositories/project-history.repository.js";
@@ -32,6 +33,8 @@ export const circuitGroupStructureProjectCommandStore =
new CircuitGroupStructureProjectCommandRepository(db);
export const circuitGroupRenumberProjectCommandStore =
new CircuitGroupRenumberProjectCommandRepository(db);
export const circuitGroupMoveProjectCommandStore =
new CircuitGroupMoveProjectCommandRepository(db);
export const distributionBoardStructureProjectCommandStore =
new DistributionBoardStructureProjectCommandRepository(db);
export const distributionBoardComponentStructureProjectCommandStore =
@@ -71,5 +74,6 @@ export const projectCommandService = new ProjectCommandService(
distributionBoardComponentStructureProjectCommandStore,
circuitGroupStructureProjectCommandStore,
circuitGroupRenumberProjectCommandStore,
circuitGroupMoveProjectCommandStore,
projectHistoryStore
);
@@ -0,0 +1,305 @@
import path from "node:path";
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { eq } from "drizzle-orm";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import {
createDatabaseContext,
type DatabaseContext,
} from "../src/db/database-context.js";
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
import { ProjectHistoryRepository } from "../src/db/repositories/project-history.repository.js";
import { circuitDeviceRows } from "../src/db/schema/circuit-device-rows.js";
import { circuitLists } from "../src/db/schema/circuit-lists.js";
import { circuitProtectionDevices } from "../src/db/schema/circuit-protection-devices.js";
import { circuitSections } from "../src/db/schema/circuit-sections.js";
import { circuits } from "../src/db/schema/circuits.js";
import { projectRevisions } from "../src/db/schema/project-revisions.js";
import { projects } from "../src/db/schema/projects.js";
import { createCircuitGroupMoveProjectCommand } from "../src/domain/models/circuit-group-move-project-command.model.js";
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
function createTestDatabase(): DatabaseContext {
const context = createDatabaseContext(":memory:");
migrate(context.db, {
migrationsFolder: path.resolve("src", "db", "migrations"),
});
context.db
.insert(projects)
.values({ id: "project-1", name: "Projekt" })
.run();
new DistributionBoardFixtureRepository(
context.db
).createWithCircuitListAndDefaultSections("project-1", "UV-01");
const list = context.db.select().from(circuitLists).get()!;
const source = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.category, "lighting"))
.get()!;
context.db
.insert(circuitSections)
.values({
id: "lighting-move-target",
circuitListId: list.id,
key: "lighting_2",
displayName: "Beleuchtung 2",
prefix: "-1F2.",
sortOrder: 40,
category: "lighting",
groupNumber: 2,
})
.run();
context.db
.insert(circuits)
.values([
{
id: "circuit-move",
circuitListId: list.id,
sectionId: source.id,
equipmentIdentifier: "-1F1.7",
displayName: "Zu verschieben",
sortOrder: 20,
},
{
id: "circuit-target-existing",
circuitListId: list.id,
sectionId: "lighting-move-target",
equipmentIdentifier: "-1F2.4",
displayName: "Bestand",
sortOrder: 10,
},
])
.run();
context.db
.insert(circuitDeviceRows)
.values({
id: "row-move",
circuitId: "circuit-move",
sortOrder: 10,
name: "Leuchte",
displayName: "Leuchte",
quantity: 2,
powerPerUnit: 0.1,
simultaneityFactor: 1,
})
.run();
context.db
.insert(circuitProtectionDevices)
.values({
circuitId: "circuit-move",
type: "LS",
ratedCurrentA: 10,
tripCharacteristic: "B",
})
.run();
return context;
}
function createMoveCommand(context: DatabaseContext) {
const circuit = context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get()!;
const source = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.id, circuit.sectionId))
.get()!;
const target = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.id, "lighting-move-target"))
.get()!;
return createCircuitGroupMoveProjectCommand(
createCircuitGroupMovePlan({
circuit,
sourceGroup: {
id: source.id,
category: source.category!,
groupNumber: source.groupNumber!,
prefix: source.prefix,
},
targetGroup: {
id: target.id,
category: target.category!,
groupNumber: target.groupNumber!,
prefix: target.prefix,
},
targetCircuitEquipmentIdentifiers: context.db
.select({ value: circuits.equipmentIdentifier })
.from(circuits)
.where(eq(circuits.sectionId, target.id))
.all()
.map(({ value }) => value),
targetSortOrder: 30,
})
);
}
describe("circuit group-move project command", () => {
it("moves a complete circuit while preserving rows and protection through undo/redo", () => {
const context = createTestDatabase();
try {
const command = createMoveCommand(context);
const repository =
new CircuitGroupMoveProjectCommandRepository(context.db);
const moved = repository.execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
});
assert.deepEqual(
context.db
.select({
sectionId: circuits.sectionId,
equipmentIdentifier: circuits.equipmentIdentifier,
sortOrder: circuits.sortOrder,
})
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get(),
{
sectionId: "lighting-move-target",
equipmentIdentifier: "-1F2.5",
sortOrder: 30,
}
);
assert.equal(
context.db
.select()
.from(circuitDeviceRows)
.where(eq(circuitDeviceRows.id, "row-move"))
.get()?.circuitId,
"circuit-move"
);
assert.equal(
context.db
.select()
.from(circuitProtectionDevices)
.where(eq(circuitProtectionDevices.circuitId, "circuit-move"))
.get()?.ratedCurrentA,
10
);
repository.execute({
projectId: "project-1",
expectedRevision: 1,
source: "undo",
historyTargetChangeSetId: new ProjectHistoryRepository(
context.db
).getNextCommand("project-1", "undo")?.changeSetId,
command: moved.inverse,
});
assert.equal(
context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get()?.equipmentIdentifier,
"-1F1.7"
);
repository.execute({
projectId: "project-1",
expectedRevision: 2,
source: "redo",
historyTargetChangeSetId: new ProjectHistoryRepository(
context.db
).getNextCommand("project-1", "redo")?.changeSetId,
command,
});
assert.equal(
context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get()?.equipmentIdentifier,
"-1F2.5"
);
} finally {
context.close();
}
});
it("rejects stale state without moving the circuit", () => {
const context = createTestDatabase();
try {
const command = createMoveCommand(context);
context.db
.update(circuits)
.set({ sortOrder: 99 })
.where(eq(circuits.id, "circuit-move"))
.run();
assert.throws(
() =>
new CircuitGroupMoveProjectCommandRepository(context.db).execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
}),
/changed before group move/
);
assert.equal(
context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get()?.sectionId,
command.payload.expectedSectionId
);
assert.equal(
context.db.select().from(projectRevisions).all().length,
0
);
} finally {
context.close();
}
});
it("rolls back the circuit move when history persistence fails", () => {
const context = createTestDatabase();
try {
const command = createMoveCommand(context);
context.sqlite.exec(`
CREATE TRIGGER fail_circuit_group_move_history
BEFORE INSERT ON project_history_stack_entries
BEGIN
SELECT RAISE(ABORT, 'forced circuit group move history failure');
END;
`);
assert.throws(
() =>
new CircuitGroupMoveProjectCommandRepository(context.db).execute({
projectId: "project-1",
expectedRevision: 0,
source: "user",
command,
}),
/forced circuit group move history failure/
);
assert.deepEqual(
context.db
.select({
sectionId: circuits.sectionId,
equipmentIdentifier: circuits.equipmentIdentifier,
sortOrder: circuits.sortOrder,
})
.from(circuits)
.where(eq(circuits.id, "circuit-move"))
.get(),
{
sectionId: command.payload.expectedSectionId,
equipmentIdentifier: "-1F1.7",
sortOrder: 20,
}
);
} finally {
context.close();
}
});
});
+1
View File
@@ -12,6 +12,7 @@ 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", () => {
+77
View File
@@ -16,6 +16,7 @@ import { CircuitSectionRenumberProjectCommandRepository } from "../src/db/reposi
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
import { CircuitGroupStructureProjectCommandRepository } from "../src/db/repositories/circuit-group-structure-project-command.repository.js";
import { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js";
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
@@ -56,6 +57,8 @@ import {
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
import { createCircuitGroupRenumberProjectCommand } from "../src/domain/models/circuit-group-renumber-project-command.model.js";
import { createCircuitGroupRenumberPlan } from "../src/domain/services/circuit-group-renumbering.js";
import { createCircuitGroupMoveProjectCommand } from "../src/domain/models/circuit-group-move-project-command.model.js";
import { createCircuitGroupMovePlan } from "../src/domain/services/circuit-group-move-planning.js";
import {
createDistributionBoardInsertProjectCommand,
createDistributionBoardStructureSnapshot,
@@ -142,6 +145,7 @@ function createService(context: DatabaseContext) {
),
new CircuitGroupStructureProjectCommandRepository(context.db),
new CircuitGroupRenumberProjectCommandRepository(context.db),
new CircuitGroupMoveProjectCommandRepository(context.db),
new ProjectHistoryRepository(context.db)
);
}
@@ -1370,6 +1374,79 @@ describe("project command service", () => {
}
});
it("dispatches same-category circuit group moves", () => {
const context = createTestDatabase();
try {
const service = createService(context);
const source = context.db
.select()
.from(circuitSections)
.where(eq(circuitSections.category, "three_phase"))
.get()!;
const target = {
id: "group-move-service-target",
circuitListId: source.circuitListId,
key: "three_phase_2",
displayName: "3-phasig 2",
prefix: "-3F2.",
sortOrder: 40,
category: "three_phase" as const,
groupNumber: 2,
};
service.executeUser({
projectId: "project-1",
expectedRevision: 0,
command: createCircuitGroupInsertProjectCommand(target),
});
context.db
.insert(circuits)
.values({
id: "circuit-group-move-service",
circuitListId: source.circuitListId,
sectionId: source.id,
equipmentIdentifier: "-3F1.1",
displayName: "Drehstrom",
sortOrder: 10,
})
.run();
const result = service.executeUser({
projectId: "project-1",
expectedRevision: 1,
command: createCircuitGroupMoveProjectCommand(
createCircuitGroupMovePlan({
circuit: {
id: "circuit-group-move-service",
circuitListId: source.circuitListId,
sectionId: source.id,
equipmentIdentifier: "-3F1.1",
sortOrder: 10,
},
sourceGroup: {
id: source.id,
category: "three_phase",
groupNumber: 1,
prefix: "-3F1.",
},
targetGroup: target,
targetCircuitEquipmentIdentifiers: [],
targetSortOrder: 20,
})
),
});
assert.equal(result.history.currentRevision, 2);
assert.equal(
context.db
.select()
.from(circuits)
.where(eq(circuits.id, "circuit-group-move-service"))
.get()?.equipmentIdentifier,
"-3F2.1"
);
} finally {
context.close();
}
});
it("dispatches floor and room setup with their persisted inverses", () => {
const context = createTestDatabase();
try {
@@ -16,6 +16,7 @@ import { CircuitSectionReorderProjectCommandRepository } from "../src/db/reposit
import { CircuitStructureProjectCommandRepository } from "../src/db/repositories/circuit-structure-project-command.repository.js";
import { CircuitGroupStructureProjectCommandRepository } from "../src/db/repositories/circuit-group-structure-project-command.repository.js";
import { CircuitGroupRenumberProjectCommandRepository } from "../src/db/repositories/circuit-group-renumber-project-command.repository.js";
import { CircuitGroupMoveProjectCommandRepository } from "../src/db/repositories/circuit-group-move-project-command.repository.js";
import { DistributionBoardFixtureRepository } from "./support/distribution-board-fixture.js";
import { DistributionBoardStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-structure-project-command.repository.js";
import { DistributionBoardComponentStructureProjectCommandRepository } from "../src/db/repositories/distribution-board-component-structure-project-command.repository.js";
@@ -199,6 +200,7 @@ function createService(context: DatabaseContext) {
),
new CircuitGroupStructureProjectCommandRepository(context.db),
new CircuitGroupRenumberProjectCommandRepository(context.db),
new CircuitGroupMoveProjectCommandRepository(context.db),
new ProjectHistoryRepository(context.db)
);
}
+6
View File
@@ -169,6 +169,12 @@ describe("project version history presentation", () => {
),
"Stromkreisgruppen neu nummeriert"
);
assert.equal(
getProjectRevisionDescription(
revision(13, { commandType: "circuit.move-group" })
),
"Stromkreis in andere Gruppe verschoben"
);
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
assert.equal(
getProjectSnapshotKindLabel("automatic"),