82 lines
9 KiB
TypeScript
82 lines
9 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import ExcelJS from "exceljs";
|
|
import { createExternalXlsxFollowUpImportPlan } from "../src/external-model/application/external-xlsx-follow-up-import.js";
|
|
import { buildExternalXlsxFollowUpTarget } from "../src/external-model/application/external-xlsx-follow-up-target.js";
|
|
import { createExternalXlsxInitialImportPlan } from "../src/external-model/application/external-xlsx-initial-import.js";
|
|
import { buildExternalXlsxInitialImportTarget } from "../src/external-model/application/external-xlsx-initial-import-target.js";
|
|
import { createDefaultExternalModelConfiguration } from "../src/external-model/configuration/external-model-configuration.js";
|
|
import type { ExternalXlsxReturnValues } from "../src/external-model/xlsx/external-xlsx-contracts.js";
|
|
import { createRevitXlsxFixture, externalXlsxTestConfiguration, firstWorksharingId, secondWorksharingId } from "./fixtures/revit-xlsx-fixtures.js";
|
|
|
|
const deviceTypeA = "10000000-0000-4000-8000-000000000001";
|
|
const deviceTypeB = "10000000-0000-4000-8000-000000000002";
|
|
const newWorksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000003";
|
|
const catalogs = { floors: [], rooms: [], distributionBoards: [], projectDevices: [] };
|
|
const configuration = { ...createDefaultExternalModelConfiguration(), ...externalXlsxTestConfiguration, deviceTypes: [{ id: deviceTypeA, name: "Steckdose", category: "single_phase" as const, defaults: { powerPerUnitW: null, simultaneityFactor: 1, cosPhi: 0.9, costGroup: null } }, { id: deviceTypeB, name: "Leuchte", category: "lighting" as const, defaults: { powerPerUnitW: 40, simultaneityFactor: 1, cosPhi: null, costGroup: null } }], familyTypeRules: [{ id: "20000000-0000-4000-8000-000000000001", exactFamilyAndType: "Steckdose: Standard", deviceTypeId: deviceTypeA, quantityRule: "socketCount" as const }, { id: "20000000-0000-4000-8000-000000000002", exactFamilyAndType: "Leuchte: Decke", deviceTypeId: deviceTypeB, quantityRule: "one" as const }] };
|
|
|
|
async function initialState() {
|
|
const bytes = await createRevitXlsxFixture();
|
|
const { plan, document } = await createExternalXlsxInitialImportPlan({ fileName: "initial.xlsx", bytes, configuration, catalogs });
|
|
let sequence = 0;
|
|
const state = buildExternalXlsxInitialImportTarget({ projectId: "project-1", expectedRevision: 0, sourceName: "Revit", importedAtIso: "2026-09-30T08:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, roomDecisions: plan.sourceRooms.map((room) => ({ sourceRoomKey: room.sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.familyGroups.map((family) => ({ familyAndType: family.familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, createId: () => `30000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
|
const missing = structuredClone(state.objects[1]!); missing.id = "30000000-0000-4000-8000-999999999999"; missing.worksharingId = "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE-00000099";
|
|
state.objects.push(missing);
|
|
return state;
|
|
}
|
|
|
|
async function changedWorkbook(firstId = firstWorksharingId) {
|
|
const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(await createRevitXlsxFixture());
|
|
const sheet = workbook.worksheets[0]!;
|
|
sheet.getCell(3, 1).value = firstId; sheet.getCell(3, 6).value = 3;
|
|
sheet.getCell(5, 1).value = newWorksharingId; sheet.getCell(5, 2).value = "Leuchte neu"; sheet.getCell(5, 5).value = "Leuchte: Decke"; sheet.getCell(5, 6).value = 1;
|
|
return new Uint8Array(await workbook.xlsx.writeBuffer());
|
|
}
|
|
|
|
function currentReturns(state: Awaited<ReturnType<typeof initialState>>) {
|
|
const map = new Map<string, ExternalXlsxReturnValues>();
|
|
for (const object of state.objects) map.set(object.id, { circuitCombined: object.acceptedSourceValues.circuitCombined, boardName: object.acceptedSourceValues.boardName, circuitIdentifier: object.acceptedSourceValues.circuitIdentifier, designation: object.acceptedSourceValues.designation, electricalPowerW: object.acceptedSourceValues.electricalPowerW, voltageV: object.acceptedSourceValues.voltageV, currentA: object.acceptedSourceValues.currentA });
|
|
map.set(state.objects[0]!.id, { ...map.get(state.objects[0]!.id)!, designation: "Noch nicht in Revit" });
|
|
return map;
|
|
}
|
|
|
|
describe("Linear XLSX follow-up import", () => {
|
|
it("classifies new, changed, unchanged, missing and return-deviating objects with exact IDs", async () => {
|
|
const state = await initialState();
|
|
const bytes = await changedWorkbook();
|
|
const { plan } = await createExternalXlsxFollowUpImportPlan({ fileName: "follow-up.xlsx", bytes, configuration, catalogs, currentState: state, currentReturnValuesByObjectId: currentReturns(state) });
|
|
assert.deepEqual({ new: plan.newObjectCount, changed: plan.changedObjectCount, unchanged: plan.unchangedObjectCount, missing: plan.missingObjectCount, returnDeviation: plan.returnDeviationCount }, { new: 1, changed: 1, unchanged: 1, missing: 1, returnDeviation: 1 });
|
|
assert.deepEqual(plan.changes[0]!.changedFields, ["socketCount"]);
|
|
assert.equal(plan.missingObjects[0]!.worksharingId.endsWith("99"), true);
|
|
|
|
const caseChanged = await createExternalXlsxFollowUpImportPlan({ fileName: "follow-up.xlsx", bytes: await changedWorkbook(firstWorksharingId.toLowerCase()), configuration, catalogs, currentState: state, currentReturnValuesByObjectId: currentReturns(state) });
|
|
assert.equal(caseChanged.plan.newObjectCount, 2);
|
|
assert.equal(caseChanged.plan.missingObjectCount, 2);
|
|
});
|
|
|
|
it("updates accepted quantities and row totals while preserving missing links and compacting old batches", async () => {
|
|
const state = await initialState();
|
|
const first = state.objects.find((object) => object.worksharingId === firstWorksharingId)!;
|
|
first.circuitDeviceRowId = "row-1";
|
|
const bytes = await changedWorkbook();
|
|
const { plan, document } = await createExternalXlsxFollowUpImportPlan({ fileName: "follow-up.xlsx", bytes, configuration, catalogs, currentState: state, currentReturnValuesByObjectId: currentReturns(state) });
|
|
let sequence = 0;
|
|
const row = { id: "row-1", circuitId: "circuit-1", linkedProjectDeviceId: null, sortOrder: 10, name: "Steckdose", displayName: "Steckdose", phaseType: "single_phase", connectionKind: null, costGroup: null, category: "single_phase", level: null, roomId: null, roomNumberSnapshot: null, roomNameSnapshot: null, quantity: 3, manualQuantity: 1, powerPerUnit: 0.125, simultaneityFactor: 1, cosPhi: 0.9, remark: null, overriddenFields: null };
|
|
const built = buildExternalXlsxFollowUpTarget({ projectId: "project-1", expectedRevision: 1, importedAtIso: "2026-09-30T09:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, currentState: state, sourceChangeDecisions: plan.changes.map((change) => ({ objectId: change.objectId, accept: true })), roomDecisions: plan.newSourceRoomKeys.map((sourceRoomKey) => ({ sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.newFamilyAndTypes.map((familyAndType) => ({ familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map([[row.id, row]]), createId: () => `40000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
|
assert.equal(built.rowTransitions[0]!.target.quantity, 4);
|
|
assert.equal(built.targetState.objects.find((object) => object.id === first.id)!.planningValues.effectiveQuantity, 3);
|
|
const missing = built.targetState.objects.find((object) => object.worksharingId.endsWith("99"))!;
|
|
assert.equal(missing.presenceStatus, "missing");
|
|
assert.equal(missing.circuitDeviceRowId, null);
|
|
assert.equal(built.targetState.importBatches[0]!.originalContentBase64, null);
|
|
assert.ok(built.targetState.importBatches[1]!.originalContentBase64);
|
|
|
|
const rejected = buildExternalXlsxFollowUpTarget({ projectId: "project-1", expectedRevision: 1, importedAtIso: "2026-09-30T09:00:00.000Z", configuration, originalContentBase64: Buffer.from(bytes).toString("base64"), document, plan, currentState: state, sourceChangeDecisions: plan.changes.map((change) => ({ objectId: change.objectId, accept: false })), roomDecisions: plan.newSourceRoomKeys.map((sourceRoomKey) => ({ sourceRoomKey, roomId: null, defaultDistributionBoardId: null })), familyProjectDeviceDecisions: plan.newFamilyAndTypes.map((familyAndType) => ({ familyAndType, projectDeviceId: null })), objectsWithoutRoomDefaultDistributionBoardId: null, rowsById: new Map([[row.id, row]]), createId: () => `50000000-0000-4000-8000-${String(++sequence).padStart(12, "0")}` });
|
|
const rejectedObject = rejected.targetState.objects.find((object) => object.id === first.id)!;
|
|
assert.equal(rejectedObject.planningValues.effectiveQuantity, first.planningValues.effectiveQuantity);
|
|
assert.equal(rejectedObject.lastAcceptedImportBatchId, first.lastAcceptedImportBatchId);
|
|
assert.notEqual(rejectedObject.lastSeenImportBatchId, first.lastSeenImportBatchId);
|
|
assert.equal(rejected.rowTransitions.length, 0);
|
|
});
|
|
});
|
|
|