forked from jappel/leistungsbilanz-ts
Add atomic Revit assignment-plan command and route
- external-object.apply-assignment-plan creates planned circuits (BMK, default protection), external-only rows and quantity changes with all object links in one revision; the history-only revert removes only unchanged created structure. - POST /external-model/objects/assignment-plan validates strictly with zod, re-checks rows with the shared planning rules and requires explicit confirmation of planning conflicts and socket-limit overruns. - The compact object list carries the socket limit and socket device types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4c44001e1
commit
fd45a1caeb
15 changed files with 1598 additions and 15 deletions
|
|
@ -0,0 +1,262 @@
|
|||
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 AppDatabase } from "../src/db/database-context.js";
|
||||
import { ExternalObjectAssignmentPlanProjectCommandRepository } from "../src/db/repositories/external-object-assignment-plan-project-command.repository.js";
|
||||
import { toCircuitDeviceRowSnapshot } from "../src/db/repositories/circuit-device-row-structure.persistence.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 { distributionBoards } from "../src/db/schema/distribution-boards.js";
|
||||
import { externalImportBatches } from "../src/db/schema/external-import-batches.js";
|
||||
import { externalModelObjects } from "../src/db/schema/external-model-objects.js";
|
||||
import { externalModelSources } from "../src/db/schema/external-model-sources.js";
|
||||
import { externalRoomMappings } from "../src/db/schema/external-room-mappings.js";
|
||||
import { floors } from "../src/db/schema/floors.js";
|
||||
import { projects } from "../src/db/schema/projects.js";
|
||||
import { rooms } from "../src/db/schema/rooms.js";
|
||||
import type { CircuitDeviceRowSnapshot } from "../src/domain/models/circuit-device-row-structure-project-command.model.js";
|
||||
import type { CircuitSnapshot } from "../src/domain/models/circuit-structure-project-command.model.js";
|
||||
import {
|
||||
createExternalObjectAssignmentPlanProjectCommand,
|
||||
externalObjectApplyAssignmentPlanCommandType,
|
||||
externalObjectRevertAssignmentPlanCommandType,
|
||||
} from "../src/domain/models/external-object-assignment-plan-project-command.model.js";
|
||||
import type { ExternalModelObjectSnapshot } from "../src/external-model/domain/external-model-contracts.js";
|
||||
|
||||
function createFixture() {
|
||||
const context = createDatabaseContext(":memory:");
|
||||
migrate(context.db, { migrationsFolder: path.resolve("src", "db", "migrations") });
|
||||
const database = context.db;
|
||||
database.insert(projects).values({ id: "project-1", name: "Projekt" }).run();
|
||||
database.insert(floors).values({ id: "floor-1", projectId: "project-1", name: "EG", sortOrder: 10 }).run();
|
||||
for (const [id, number] of [["room-1", "101"], ["room-2", "102"]] as const) {
|
||||
database.insert(rooms).values({ id, projectId: "project-1", floorId: "floor-1", roomNumber: number, roomName: "Büro" }).run();
|
||||
}
|
||||
database.insert(distributionBoards).values({ id: "board-1", projectId: "project-1", name: "UV 1" }).run();
|
||||
database.insert(circuitLists).values({ id: "list-1", projectId: "project-1", distributionBoardId: "board-1", name: "Liste" }).run();
|
||||
database.insert(circuitSections).values({ id: "section-1", circuitListId: "list-1", key: "single-1", displayName: "1-phasig", prefix: "-2F1.", sortOrder: 10, category: "single_phase", groupNumber: 1 }).run();
|
||||
database.insert(circuits).values({ id: "circuit-1", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-2F1.1", displayName: "Steckdose", sortOrder: 10, voltage: 230 }).run();
|
||||
database.insert(circuits).values({ id: "circuit-2", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-2F1.2", displayName: "Reserve", sortOrder: 20, voltage: 230, isReserve: 1 }).run();
|
||||
database.insert(circuitDeviceRows).values(rowValues({ id: "row-1", circuitId: "circuit-1", quantity: 2 })).run();
|
||||
database.insert(externalModelSources).values({ id: "source-1", projectId: "project-1", name: "Revit", sourceType: "revit_xlsx" }).run();
|
||||
database.insert(externalImportBatches).values({
|
||||
id: "batch-1", projectId: "project-1", sourceId: "source-1", importKind: "initial",
|
||||
importedAtIso: "2026-10-30T10:00:00.000Z", fileName: "revit.xlsx", sha256: "a".repeat(64), objectCount: 4,
|
||||
}).run();
|
||||
for (const [id, roomId, number] of [["mapping-1", "room-1", "101"], ["mapping-2", "room-2", "102"]] as const) {
|
||||
database.insert(externalRoomMappings).values({
|
||||
id, projectId: "project-1", sourceId: "source-1", normalizedSourceRoomKey: `number:${number}`,
|
||||
sourceFloorName: "EG", sourceRoomNumber: number, sourceRoomName: "Büro", roomId, defaultDistributionBoardId: "board-1",
|
||||
}).run();
|
||||
}
|
||||
database.insert(externalModelObjects).values([
|
||||
objectSnapshot("object-0", "mapping-1", "row-1"),
|
||||
objectSnapshot("object-a1", "mapping-1", null),
|
||||
objectSnapshot("object-a2", "mapping-1", null),
|
||||
objectSnapshot("object-b1", "mapping-2", null),
|
||||
]).run();
|
||||
return context;
|
||||
}
|
||||
|
||||
function rowValues(overrides: Partial<CircuitDeviceRowSnapshot> & { id: string; circuitId: string }): CircuitDeviceRowSnapshot {
|
||||
return {
|
||||
linkedProjectDeviceId: null, sortOrder: 10, name: "Steckdose: Doppel", displayName: "Steckdose",
|
||||
phaseType: "single_phase", connectionKind: "socket", costGroup: null, category: "single_phase", level: null,
|
||||
roomId: "room-1", roomNumberSnapshot: "101", roomNameSnapshot: "Büro", quantity: 2, manualQuantity: 0,
|
||||
powerPerUnit: 0.12, simultaneityFactor: 1, cosPhi: null, remark: null, overriddenFields: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const worksharingIds: Record<string, string> = {
|
||||
"object-0": "00000000-0000-4000-8000-000000000000-00000001",
|
||||
"object-a1": "00000000-0000-4000-8000-000000000001-00000001",
|
||||
"object-a2": "00000000-0000-4000-8000-000000000002-00000001",
|
||||
"object-b1": "00000000-0000-4000-8000-000000000003-00000001",
|
||||
};
|
||||
|
||||
function objectSnapshot(id: string, mappingId: string, circuitDeviceRowId: string | null): ExternalModelObjectSnapshot {
|
||||
const roomNumber = mappingId === "mapping-1" ? "101" : "102";
|
||||
return {
|
||||
id, projectId: "project-1", sourceId: "source-1", worksharingId: worksharingIds[id]!, ifcGuid: null,
|
||||
deviceTypeId: "device-type-1", lastSeenImportBatchId: "batch-1", lastAcceptedImportBatchId: "batch-1",
|
||||
acceptedSourceValues: {
|
||||
rowNumber: 3, roomNumber, roomName: "Büro", familyAndType: "Steckdose: Doppel", selectionMarker: "SD",
|
||||
socketCount: "2", sourceRemark: null, circuitCombined: null, boardName: null, circuitIdentifier: null,
|
||||
designation: null, electricalPowerW: null, voltageV: null, currentA: null,
|
||||
},
|
||||
planningValues: {
|
||||
displayName: "Steckdose", category: "single_phase", connectionKind: "socket", effectiveQuantity: 2,
|
||||
powerPerUnitW: 120, simultaneityFactor: 1, cosPhi: null, costGroup: null, remark: null,
|
||||
},
|
||||
overriddenFields: [], externalRoomMappingId: mappingId, distributionBoardId: "board-1",
|
||||
linkedProjectDeviceId: null, circuitDeviceRowId, presenceStatus: "present",
|
||||
};
|
||||
}
|
||||
|
||||
function link(id: string, mappingId: string, rowId: string) {
|
||||
const expected = objectSnapshot(id, mappingId, null);
|
||||
return { expected, target: { ...expected, circuitDeviceRowId: rowId } };
|
||||
}
|
||||
|
||||
function planCommand(database: AppDatabase) {
|
||||
const existing = toCircuitDeviceRowSnapshot(
|
||||
database.select().from(circuitDeviceRows).where(eq(circuitDeviceRows.id, "row-1")).get()!
|
||||
);
|
||||
const newCircuit: CircuitSnapshot = {
|
||||
id: "circuit-new", circuitListId: "list-1", sectionId: "section-1", equipmentIdentifier: "-2F1.3",
|
||||
displayName: "Steckdose", sortOrder: 30, cableType: null, cableCrossSection: null, cableLength: null,
|
||||
rcdAssignment: null, terminalDesignation: null, voltage: 230, controlRequirement: null, status: null,
|
||||
isReserve: false, remark: null,
|
||||
deviceRows: [rowValues({ id: "row-new-b", circuitId: "circuit-new", roomId: "room-2", roomNumberSnapshot: "102" })],
|
||||
protectionDevice: {
|
||||
circuitId: "circuit-new", type: "FI_LS", ratedCurrentA: 16, fuseUtilizationCategory: null,
|
||||
tripCharacteristic: "B", rcdType: "A", ratedResidualCurrentMa: 30,
|
||||
},
|
||||
};
|
||||
return createExternalObjectAssignmentPlanProjectCommand(externalObjectApplyAssignmentPlanCommandType, {
|
||||
newCircuits: [newCircuit],
|
||||
newRows: [rowValues({ id: "row-new-a", circuitId: "circuit-2" })],
|
||||
rows: [{ expected: existing, target: { ...existing, quantity: 4 } }],
|
||||
objects: [
|
||||
link("object-a1", "mapping-1", "row-1"),
|
||||
link("object-a2", "mapping-1", "row-new-a"),
|
||||
link("object-b1", "mapping-2", "row-new-b"),
|
||||
],
|
||||
confirmedConflictObjectIds: [],
|
||||
});
|
||||
}
|
||||
|
||||
function readState(database: AppDatabase) {
|
||||
const circuitRows = database.select().from(circuits).all()
|
||||
.map(({ id, displayName, isReserve, equipmentIdentifier }) => ({ id, displayName, isReserve, equipmentIdentifier }))
|
||||
.sort((left, right) => left.id.localeCompare(right.id));
|
||||
const rows = database.select().from(circuitDeviceRows).all()
|
||||
.map(({ id, circuitId, quantity }) => ({ id, circuitId, quantity }))
|
||||
.sort((left, right) => left.id.localeCompare(right.id));
|
||||
const links = Object.fromEntries(
|
||||
database.select().from(externalModelObjects).all().map((object) => [object.id, object.circuitDeviceRowId])
|
||||
);
|
||||
const protections = database.select().from(circuitProtectionDevices).all().map((entry) => entry.circuitId);
|
||||
const revision = database.select().from(projects).get()!.currentRevision;
|
||||
return { circuitRows, rows, links, protections, revision };
|
||||
}
|
||||
|
||||
describe("external object assignment-plan project command", () => {
|
||||
it("applies a plan atomically and restores it exactly through undo and redo", () => {
|
||||
const context = createFixture();
|
||||
try {
|
||||
const repository = new ExternalObjectAssignmentPlanProjectCommandRepository(context.db);
|
||||
const before = readState(context.db);
|
||||
const applied = repository.execute({
|
||||
projectId: "project-1", expectedRevision: 0, source: "user", command: planCommand(context.db),
|
||||
});
|
||||
const after = readState(context.db);
|
||||
assert.deepEqual(after.links, {
|
||||
"object-0": "row-1", "object-a1": "row-1", "object-a2": "row-new-a", "object-b1": "row-new-b",
|
||||
});
|
||||
assert.deepEqual(after.rows.find((entry) => entry.id === "row-1"), { id: "row-1", circuitId: "circuit-1", quantity: 4 });
|
||||
assert.deepEqual(after.circuitRows.find((entry) => entry.id === "circuit-2"), {
|
||||
id: "circuit-2", displayName: "Steckdose", isReserve: 0, equipmentIdentifier: "-2F1.2",
|
||||
});
|
||||
assert.deepEqual(after.circuitRows.find((entry) => entry.id === "circuit-new")?.equipmentIdentifier, "-2F1.3");
|
||||
assert.deepEqual(after.protections, ["circuit-new"]);
|
||||
assert.equal(applied.inverse.type, externalObjectRevertAssignmentPlanCommandType);
|
||||
assert.deepEqual(applied.inverse.payload.circuitDisplayNames, [
|
||||
{ circuitId: "circuit-2", expected: "Steckdose", target: "Reserve" },
|
||||
]);
|
||||
|
||||
const undone = repository.execute({
|
||||
projectId: "project-1", expectedRevision: 1, source: "undo",
|
||||
historyTargetChangeSetId: applied.revision.changeSetId, command: applied.inverse,
|
||||
});
|
||||
assert.deepEqual({ ...readState(context.db), revision: 0 }, { ...before, revision: 0 });
|
||||
|
||||
repository.execute({
|
||||
projectId: "project-1", expectedRevision: 2, source: "redo",
|
||||
historyTargetChangeSetId: applied.revision.changeSetId, command: undone.inverse,
|
||||
});
|
||||
assert.deepEqual({ ...readState(context.db), revision: 0 }, { ...after, revision: 0 });
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rolls back completely when an object changed and rejects a user revert", () => {
|
||||
const context = createFixture();
|
||||
try {
|
||||
const repository = new ExternalObjectAssignmentPlanProjectCommandRepository(context.db);
|
||||
const command = planCommand(context.db);
|
||||
context.db.update(externalModelObjects)
|
||||
.set({ circuitDeviceRowId: "row-1" })
|
||||
.where(eq(externalModelObjects.id, "object-b1")).run();
|
||||
const before = readState(context.db);
|
||||
assert.throws(
|
||||
() => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command }),
|
||||
/changed before plan assignment/
|
||||
);
|
||||
assert.deepEqual(readState(context.db), before);
|
||||
|
||||
const revert = createExternalObjectAssignmentPlanProjectCommand(
|
||||
externalObjectRevertAssignmentPlanCommandType,
|
||||
{ ...command.payload, objects: command.payload.objects.map(({ expected, target }) => ({ expected: target, target: expected })) }
|
||||
);
|
||||
assert.throws(
|
||||
() => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: revert }),
|
||||
/only be reverted through project history/
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("refuses undo after a created row was changed", () => {
|
||||
const context = createFixture();
|
||||
try {
|
||||
const repository = new ExternalObjectAssignmentPlanProjectCommandRepository(context.db);
|
||||
const applied = repository.execute({
|
||||
projectId: "project-1", expectedRevision: 0, source: "user", command: planCommand(context.db),
|
||||
});
|
||||
context.db.update(circuitDeviceRows).set({ remark: "geändert" })
|
||||
.where(eq(circuitDeviceRows.id, "row-new-a")).run();
|
||||
const before = readState(context.db);
|
||||
assert.throws(
|
||||
() => repository.execute({
|
||||
projectId: "project-1", expectedRevision: 1, source: "undo",
|
||||
historyTargetChangeSetId: applied.revision.changeSetId, command: applied.inverse,
|
||||
}),
|
||||
/changed before history removal/
|
||||
);
|
||||
assert.deepEqual(readState(context.db), before);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("requires explicit confirmation of planning-value conflicts on existing rows", () => {
|
||||
const context = createFixture();
|
||||
try {
|
||||
context.db.update(circuitDeviceRows).set({ simultaneityFactor: 0.5 })
|
||||
.where(eq(circuitDeviceRows.id, "row-1")).run();
|
||||
const repository = new ExternalObjectAssignmentPlanProjectCommandRepository(context.db);
|
||||
assert.throws(
|
||||
() => repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: planCommand(context.db) }),
|
||||
/explicit conflict confirmation/
|
||||
);
|
||||
const confirmed = createExternalObjectAssignmentPlanProjectCommand(
|
||||
externalObjectApplyAssignmentPlanCommandType,
|
||||
{ ...planCommand(context.db).payload, confirmedConflictObjectIds: ["object-a1"] }
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
repository.execute({ projectId: "project-1", expectedRevision: 0, source: "user", command: confirmed })
|
||||
);
|
||||
} finally {
|
||||
context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
206
tests/external-object-assignment-plan-route.test.ts
Normal file
206
tests/external-object-assignment-plan-route.test.ts
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
/**
|
||||
* HTTP boundary of the Revit assignment plan route against createApp() with
|
||||
* an in-memory database: authentication, strict input and foreign objects.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { after, before, describe, it } from "node:test";
|
||||
import config, { WRITE_HEADER, signIn } from "./security/security.config.ts";
|
||||
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
describe("Revit assignment plan route", () => {
|
||||
let server: import("node:http").Server;
|
||||
let baseUrl = "";
|
||||
let user: Headers = {};
|
||||
|
||||
async function call(method: string, path: string, headers: Headers, body?: unknown) {
|
||||
return fetch(baseUrl + path, {
|
||||
method,
|
||||
headers: { ...headers, "content-type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function createProjectWithBoard(name: string) {
|
||||
const project = (await (await call("POST", "/api/projects", user, { name })).json()) as { id: string };
|
||||
const created = await call("POST", `/api/projects/${project.id}/distribution-boards`, user, {
|
||||
expectedRevision: 0, name: "UV-EG", floorId: null, supplyType: "AV",
|
||||
});
|
||||
assert.equal(created.status, 201, await created.clone().text());
|
||||
const [circuitList] = (await (await call("GET", `/api/projects/${project.id}/circuit-lists`, user)).json()) as Array<{ id: string }>;
|
||||
return { projectId: project.id, circuitListId: circuitList!.id };
|
||||
}
|
||||
|
||||
function plan(circuitListId: string, objectId = "unknown-object") {
|
||||
return {
|
||||
expectedRevision: 1,
|
||||
circuitListId,
|
||||
newCircuits: [],
|
||||
rows: [{ kind: "new-row", circuitId: "unknown-circuit", objectIds: [objectId] }],
|
||||
confirmedConflictObjectIds: [],
|
||||
confirmedOverLimitCircuitKeys: [],
|
||||
};
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
const app = await config.createApp();
|
||||
server = app.listen(0, "127.0.0.1");
|
||||
await new Promise((resolve) => server.once("listening", resolve));
|
||||
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
||||
user = await signIn(baseUrl, "planer-a");
|
||||
});
|
||||
|
||||
after(() => new Promise<void>((resolve) => server.close(() => resolve())));
|
||||
|
||||
async function seedSockets(projectId: string, boardId: string, count: number) {
|
||||
const { db } = await import("../src/db/client.js");
|
||||
const { externalModelConfigurations } = await import("../src/db/schema/external-model-configurations.js");
|
||||
const { externalModelSources } = await import("../src/db/schema/external-model-sources.js");
|
||||
const { externalImportBatches } = await import("../src/db/schema/external-import-batches.js");
|
||||
const { externalModelObjects } = await import("../src/db/schema/external-model-objects.js");
|
||||
const { createDefaultExternalModelConfiguration } = await import(
|
||||
"../src/external-model/configuration/external-model-configuration.js"
|
||||
);
|
||||
const deviceTypeId = "10000000-0000-4000-8000-000000000001";
|
||||
db.insert(externalModelConfigurations).values({
|
||||
id: `config-${projectId}`,
|
||||
projectId,
|
||||
configuration: {
|
||||
...createDefaultExternalModelConfiguration(),
|
||||
deviceTypes: [{
|
||||
id: deviceTypeId, name: "Steckdose", category: "single_phase", isSocket: true,
|
||||
defaults: { powerPerUnitW: 200, simultaneityFactor: 1, cosPhi: null, costGroup: null },
|
||||
}],
|
||||
familyTypeRules: [],
|
||||
},
|
||||
}).run();
|
||||
db.insert(externalModelSources).values({ id: `source-${projectId}`, projectId, name: "Revit", sourceType: "revit_xlsx" }).run();
|
||||
db.insert(externalImportBatches).values({
|
||||
id: `batch-${projectId}`, projectId, sourceId: `source-${projectId}`, importKind: "initial",
|
||||
importedAtIso: "2026-10-30T10:00:00.000Z", fileName: "revit.xlsx", sha256: "b".repeat(64), objectCount: count,
|
||||
}).run();
|
||||
const ids = Array.from({ length: count }, (_, index) => `${projectId}-socket-${index + 1}`);
|
||||
db.insert(externalModelObjects).values(ids.map((id, index) => ({
|
||||
id, projectId, sourceId: `source-${projectId}`,
|
||||
worksharingId: `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}-00000001`,
|
||||
ifcGuid: null, deviceTypeId, lastSeenImportBatchId: `batch-${projectId}`, lastAcceptedImportBatchId: `batch-${projectId}`,
|
||||
acceptedSourceValues: {
|
||||
rowNumber: index + 3, roomNumber: null, roomName: null, familyAndType: "Steckdose: Doppel", selectionMarker: "SD",
|
||||
socketCount: "2", sourceRemark: null, circuitCombined: null, boardName: null, circuitIdentifier: null,
|
||||
designation: null, electricalPowerW: null, voltageV: null, currentA: null,
|
||||
},
|
||||
planningValues: {
|
||||
displayName: "Steckdose", category: "single_phase" as const, connectionKind: null, effectiveQuantity: 2,
|
||||
powerPerUnitW: 200, simultaneityFactor: 1, cosPhi: null, costGroup: null, remark: null,
|
||||
},
|
||||
overriddenFields: [], externalRoomMappingId: null, distributionBoardId: boardId,
|
||||
linkedProjectDeviceId: null, circuitDeviceRowId: null, presenceStatus: "present" as const,
|
||||
}))).run();
|
||||
return ids;
|
||||
}
|
||||
|
||||
it("applies a split plan with the socket limit, one undo step and stable BMKs", async () => {
|
||||
const { projectId, circuitListId } = await createProjectWithBoard("Steckdosen");
|
||||
const tree = (await (await call("GET", `/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`, user)).json()) as {
|
||||
distributionBoardId: string;
|
||||
sections: Array<{ id: string; category?: string; prefix: string; circuits: Array<{ id: string; equipmentIdentifier: string; deviceRows: Array<{ quantity: number; manualQuantity: number }> }> }>;
|
||||
};
|
||||
const section = tree.sections.find((entry) => entry.category === "single_phase")!;
|
||||
const objectIds = await seedSockets(projectId, tree.distributionBoardId, 6);
|
||||
const settings = await call("PUT", `/api/projects/${projectId}`, user, {
|
||||
expectedRevision: 1, name: "Steckdosen", internalProjectNumber: null, externalProjectNumber: null,
|
||||
buildingOwner: null, description: null, isPublicBuilding: false, singlePhaseVoltageV: 230,
|
||||
threePhaseVoltageV: 400, enabledDistributionBoardSupplyTypes: ["AV", "SV", "EV", "USV", "MSR", "SiBe"],
|
||||
maxSocketsPerCircuit: 8,
|
||||
});
|
||||
assert.equal(settings.status, 200, await settings.clone().text());
|
||||
|
||||
const path = `/api/projects/${projectId}/external-model/objects/assignment-plan`;
|
||||
const request = {
|
||||
expectedRevision: 2,
|
||||
circuitListId,
|
||||
newCircuits: [{ key: "a", sectionId: section.id }, { key: "b", sectionId: section.id }],
|
||||
rows: [
|
||||
{ kind: "new-row-in-new-circuit", newCircuitKey: "a", objectIds: objectIds.slice(0, 4) },
|
||||
{ kind: "new-row-in-new-circuit", newCircuitKey: "b", objectIds: objectIds.slice(4, 6) },
|
||||
],
|
||||
confirmedConflictObjectIds: [],
|
||||
confirmedOverLimitCircuitKeys: [],
|
||||
};
|
||||
const overLimit = await call("POST", path, user, {
|
||||
...request,
|
||||
rows: [{ kind: "new-row-in-new-circuit", newCircuitKey: "a", objectIds: objectIds.slice(0, 5) }],
|
||||
newCircuits: [{ key: "a", sectionId: section.id }],
|
||||
});
|
||||
assert.equal(overLimit.status, 400);
|
||||
assert.match(await overLimit.text(), /Höchstwert von 8 Steckdosen/);
|
||||
|
||||
const applied = await call("POST", path, user, request);
|
||||
assert.equal(applied.status, 200, await applied.clone().text());
|
||||
const afterTree = (await (await call("GET", `/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`, user)).json()) as typeof tree;
|
||||
const created = afterTree.sections.find((entry) => entry.id === section.id)!.circuits;
|
||||
assert.deepEqual(created.map((circuit) => circuit.equipmentIdentifier), [`${section.prefix}1`, `${section.prefix}2`]);
|
||||
assert.deepEqual(created.map((circuit) => circuit.deviceRows.map((row) => [row.quantity, row.manualQuantity])), [[[8, 0]], [[4, 0]]]);
|
||||
|
||||
const objects = (await (await call("GET", `/api/projects/${projectId}/external-model/objects`, user)).json()) as {
|
||||
maxSocketsPerCircuit: number | null; socketDeviceTypeIds: string[]; objects: Array<{ circuitDeviceRowId: string | null }>;
|
||||
};
|
||||
assert.equal(objects.maxSocketsPerCircuit, 8);
|
||||
assert.equal(objects.socketDeviceTypeIds.length, 1);
|
||||
assert.ok(objects.objects.every((object) => object.circuitDeviceRowId !== null));
|
||||
|
||||
const undone = await call("POST", `/api/projects/${projectId}/history/undo`, user, { expectedRevision: 3 });
|
||||
assert.equal(undone.status, 200, await undone.clone().text());
|
||||
const undoneTree = (await (await call("GET", `/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`, user)).json()) as typeof tree;
|
||||
assert.equal(undoneTree.sections.find((entry) => entry.id === section.id)!.circuits.length, 0);
|
||||
const redone = await call("POST", `/api/projects/${projectId}/history/redo`, user, { expectedRevision: 4 });
|
||||
assert.equal(redone.status, 200, await redone.clone().text());
|
||||
const redoneTree = (await (await call("GET", `/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`, user)).json()) as typeof tree;
|
||||
assert.deepEqual(
|
||||
redoneTree.sections.find((entry) => entry.id === section.id)!.circuits.map((circuit) => circuit.id),
|
||||
created.map((circuit) => circuit.id)
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects requests without a session", async () => {
|
||||
const { projectId, circuitListId } = await createProjectWithBoard("Ohne Login");
|
||||
const response = await call(
|
||||
"POST",
|
||||
`/api/projects/${projectId}/external-model/objects/assignment-plan`,
|
||||
WRITE_HEADER,
|
||||
plan(circuitListId)
|
||||
);
|
||||
assert.equal(response.status, 401);
|
||||
});
|
||||
|
||||
it("rejects invalid input and unknown fields", async () => {
|
||||
const { projectId, circuitListId } = await createProjectWithBoard("Ungültig");
|
||||
const path = `/api/projects/${projectId}/external-model/objects/assignment-plan`;
|
||||
assert.equal((await call("POST", path, user, {})).status, 400);
|
||||
assert.equal((await call("POST", path, user, { ...plan(circuitListId), extra: true })).status, 400);
|
||||
assert.equal(
|
||||
(await call("POST", path, user, { ...plan(circuitListId), rows: [{ kind: "row", rowId: "x", objectIds: ["a"] }] })).status,
|
||||
400
|
||||
);
|
||||
assert.equal((await call("POST", path, user, { ...plan(circuitListId), rows: [] })).status, 400);
|
||||
});
|
||||
|
||||
it("rejects a circuit list of another project and unknown objects", async () => {
|
||||
const own = await createProjectWithBoard("Eigenes Projekt");
|
||||
const foreign = await createProjectWithBoard("Fremdes Projekt");
|
||||
const path = `/api/projects/${own.projectId}/external-model/objects/assignment-plan`;
|
||||
const foreignList = await call("POST", path, user, plan(foreign.circuitListId));
|
||||
assert.equal(foreignList.status, 404);
|
||||
const unknownObject = await call("POST", path, user, plan(own.circuitListId));
|
||||
assert.equal(unknownObject.status, 400);
|
||||
assert.match(await unknownObject.text(), /Revit-Objekt wurde nicht gefunden/);
|
||||
const missingProject = await call(
|
||||
"POST",
|
||||
"/api/projects/unknown-project/external-model/objects/assignment-plan",
|
||||
user,
|
||||
plan(own.circuitListId)
|
||||
);
|
||||
assert.equal(missingProject.status, 404);
|
||||
});
|
||||
});
|
||||
|
|
@ -125,6 +125,7 @@ const config = {
|
|||
{ method: "PUT", path: "/api/projects/{id}", body: {} },
|
||||
{ method: "PUT", path: "/api/projects/{id}", body: "{kaputt" },
|
||||
{ method: "POST", path: "/api/projects/{id}/distribution-boards/import", body: {} },
|
||||
{ method: "POST", path: "/api/projects/{id}/external-model/objects/assignment-plan", body: {} },
|
||||
{
|
||||
method: "POST",
|
||||
path: "/api/projects/{id}/distribution-boards/import",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue