import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js"; import { CircuitDeviceRowRepository } from "../src/db/repositories/circuit-device-row.repository.js"; import { CircuitRepository } from "../src/db/repositories/circuit.repository.js"; import { db } from "../src/db/client.js"; import { createCircuitSchema } from "../src/shared/validation/circuit.schemas.js"; describe("circuit write service rules", () => { it("accepts future sizing inputs without calculating sizing suggestions", () => { const parsed = createCircuitSchema.safeParse({ sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, voltage: 230, controlRequirement: "DALI", }); assert.equal(parsed.success, true); }); it("passes voltage and control requirements through circuit updates", async () => { let updatePayload: Record = {}; const circuit = { id: "c1", circuitListId: "l1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, voltage: 230, controlRequirement: "none", }; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1" } as never; }, } as never, circuitRepository: { async findById() { return circuit as never; }, async existsByEquipmentIdentifier() { return false; }, async update(_id: string, payload: Record) { updatePayload = payload; }, } as never, }); await service.updateCircuit("c1", { voltage: 400, controlRequirement: "DALI" }); assert.equal(updatePayload.voltage, 400); assert.equal(updatePayload.controlRequirement, "DALI"); }); it("rejects duplicate equipment identifiers in same circuit list", async () => { const service = new CircuitWriteService({ circuitListRepository: { async findById() { return { id: "list1", projectId: "p1" } as never; }, } as never, circuitSectionRepository: { async findById() { return { id: "sec1", circuitListId: "list1" } as never; }, } as never, circuitRepository: { async existsByEquipmentIdentifier() { return true; }, } as never, }); await assert.rejects( () => service.createCircuit("p1", "list1", { sectionId: "sec1", equipmentIdentifier: "-2F1", sortOrder: 10, }), /Duplicate equipmentIdentifier/ ); }); it("rejects section and list mismatch", async () => { const service = new CircuitWriteService({ circuitListRepository: { async findById() { return { id: "list1", projectId: "p1" } as never; }, } as never, circuitSectionRepository: { async findById() { return { id: "sec1", circuitListId: "other" } as never; }, } as never, circuitRepository: { async existsByEquipmentIdentifier() { return false; }, } as never, }); await assert.rejects( () => service.createCircuit("p1", "list1", { sectionId: "sec1", equipmentIdentifier: "-2F1", sortOrder: 10, }), /Section does not belong to circuit list/ ); }); it("deleting last device row keeps circuit and sets reserve", async () => { let transactionalDelete: { rowId: string; circuitId: string } | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, deleteFromCircuitTransactional(rowId: string, circuitId: string) { transactionalDelete = { rowId, circuitId }; }, } as never, circuitRepository: { async findById() { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-2F1", sortOrder: 10, isReserve: 0, } as never; }, } as never, }); await service.deleteDeviceRow("r1"); assert.deepEqual(transactionalDelete, { rowId: "r1", circuitId: "c1" }); }); it("creating device row in reserve circuit clears reserve status", async () => { let transactionalCreate: | { circuitId: string; sortOrder?: number; name: string; displayName: string; } | undefined; const service = new CircuitWriteService({ circuitRepository: { async findById() { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-2F1", sortOrder: 10, isReserve: 1, } as never; }, } as never, deviceRowRepository: { createInCircuitTransactional(input: typeof transactionalCreate) { transactionalCreate = input; return "row1"; }, async findById() { return { id: "row1" } as never; }, } as never, circuitListRepository: {} as never, circuitSectionRepository: {} as never, projectDeviceRepository: { async findById() { return { id: "pd1" } as never; }, } as never, }); await service.createDeviceRow("c1", { name: "Load", displayName: "Load", quantity: 1, powerPerUnit: 1, simultaneityFactor: 1, }); assert.deepEqual(transactionalCreate, { circuitId: "c1", linkedProjectDeviceId: undefined, sortOrder: undefined, name: "Load", displayName: "Load", phaseType: undefined, connectionKind: undefined, costGroup: undefined, category: undefined, level: undefined, roomId: undefined, roomNumberSnapshot: undefined, roomNameSnapshot: undefined, quantity: 1, powerPerUnit: 1, simultaneityFactor: 1, cosPhi: undefined, remark: undefined, overriddenFields: undefined, }); }); it("renumber uses safe bulk identifier update for swapped identifiers", async () => { let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = []; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1", prefix: "-2F" } as never; }, } as never, circuitRepository: { async listBySection() { return [ { id: "cB", sectionId: "s1", equipmentIdentifier: "-2F2", sortOrder: 10, isReserve: 0 }, { id: "cA", sectionId: "s1", equipmentIdentifier: "-2F1", sortOrder: 20, isReserve: 1 }, ] as never[]; }, async listByCircuitList() { return [{ id: "x1", sectionId: "s2", equipmentIdentifier: "-3F1" }] as never[]; }, async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) { safeUpdatePayload = updates; }, } as never, }); const result = await service.renumberSection("s1"); assert.deepEqual(safeUpdatePayload, [ { id: "cB", equipmentIdentifier: "-2F1" }, { id: "cA", equipmentIdentifier: "-2F2" }, ]); assert.equal(result.length, 2); }); it("renumber shifts forward/backward and respects other sections", async () => { let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = []; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never; }, } as never, circuitRepository: { async listBySection() { return [ { id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 10, isReserve: 0 }, { id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 20, isReserve: 0 }, { id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 }, ] as never[]; }, async listByCircuitList() { return [{ id: "o1", sectionId: "s2", equipmentIdentifier: "-1F2" }] as never[]; }, async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) { safeUpdatePayload = updates; }, } as never, }); await service.renumberSection("s1"); assert.deepEqual(safeUpdatePayload, [ { id: "c2", equipmentIdentifier: "-1F1" }, { id: "c1", equipmentIdentifier: "-1F3" }, { id: "c3", equipmentIdentifier: "-1F4" }, ]); }); it("renumber handles gaps and keeps device rows untouched by identifier-only update path", async () => { let safeCalled = 0; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never; }, } as never, circuitRepository: { async listBySection() { return [ { id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 }, { id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 20, isReserve: 0 }, { id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 }, ] as never[]; }, async listByCircuitList() { return [] as never[]; }, async updateEquipmentIdentifiersSafely() { safeCalled += 1; }, } as never, deviceRowRepository: { async update() { throw new Error("device rows must not be touched"); }, } as never, }); await service.renumberSection("s1"); assert.equal(safeCalled, 1); }); it("moving a device row to another circuit preserves row and toggles reserve flags", async () => { let transactionalMove: | { rows: Array<{ id: string; expectedCircuitId: string }>; targetCircuitId?: string; createTargetCircuit?: unknown; } | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, moveRowsTransactional(input: typeof transactionalMove) { transactionalMove = input; return { movedRowIds: input!.rows.map((row) => row.id), targetCircuitId: input!.targetCircuitId!, }; }, } as never, circuitRepository: { async findById(circuitId: string) { if (circuitId === "c1") { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, } as never; } return { id: "c2", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 1, } as never; }, } as never, }); await service.moveDeviceRow("r1", { targetCircuitId: "c2" }); assert.deepEqual(transactionalMove, { rows: [{ id: "r1", expectedCircuitId: "c1" }], targetCircuitId: "c2", createTargetCircuit: undefined, }); }); it("moving a device row to placeholder creates a new circuit in target section", async () => { let preparedTarget: | { circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName: string; sortOrder: number; } | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) { preparedTarget = input.createTargetCircuit; return { movedRowIds: input.rows.map((row) => row.id), targetCircuitId: "c-new", createdCircuitId: "c-new", }; }, } as never, circuitRepository: { async findById(circuitId: string) { if (circuitId === "c1") { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, } as never; } return null as never; }, async listBySection() { return [{ sortOrder: 40 }] as never[]; }, } as never, circuitSectionRepository: { async findById() { return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never; }, } as never, numberingService: { async getNextIdentifier() { return "-2F8"; }, } as never, }); await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true }); assert.deepEqual(preparedTarget, { circuitListId: "l1", sectionId: "s2", equipmentIdentifier: "-2F8", displayName: "Neuer Stromkreis", sortOrder: 50, }); }); it("moving a device row to its current circuit remains a no-op", async () => { let transactionalMoveCount = 0; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return { id: "r1", circuitId: "c1" } as never; }, moveRowsTransactional() { transactionalMoveCount += 1; throw new Error("same-circuit move must not write"); }, } as never, circuitRepository: { async findById() { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0, } as never; }, } as never, }); const result = await service.moveDeviceRow("r1", { targetCircuitId: "c1" }); assert.equal(transactionalMoveCount, 0); assert.equal(result?.id, "r1"); }); it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => { let transactionalMove: | { rows: Array<{ id: string; expectedCircuitId: string }>; targetCircuitId?: string; createTargetCircuit?: unknown; } | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById(rowId: string) { if (rowId === "r1") { return { id: "r1", circuitId: "c1" } as never; } return { id: "r2", circuitId: "c2" } as never; }, moveRowsTransactional(input: typeof transactionalMove) { transactionalMove = input; return { movedRowIds: input!.rows.map((row) => row.id), targetCircuitId: input!.targetCircuitId!, }; }, } as never, circuitRepository: { async findById(circuitId: string) { if (circuitId === "c1") { return { id: "c1", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never; } if (circuitId === "c2") { return { id: "c2", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 } as never; } return { id: "c3", sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F3", sortOrder: 30, isReserve: 1 } as never; }, } as never, }); const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetCircuitId: "c3" }); assert.deepEqual(transactionalMove, { rows: [ { id: "r1", expectedCircuitId: "c1" }, { id: "r2", expectedCircuitId: "c2" }, ], targetCircuitId: "c3", createTargetCircuit: undefined, }); assert.deepEqual(result, { movedRowIds: ["r1", "r2"], targetCircuitId: "c3", }); }); it("moving multiple device rows to placeholder creates exactly one new circuit", async () => { let transactionCount = 0; let preparedTarget: | { circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName: string; sortOrder: number; } | undefined; const service = new CircuitWriteService({ deviceRowRepository: { async findById(rowId: string) { return { id: rowId, circuitId: rowId === "r1" ? "c1" : "c2" } as never; }, moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) { transactionCount += 1; preparedTarget = input.createTargetCircuit; return { movedRowIds: input.rows.map((row) => row.id), targetCircuitId: "c-new", createdCircuitId: "c-new", }; }, } as never, circuitRepository: { async findById(circuitId: string) { if (circuitId === "c1" || circuitId === "c2") { return { id: circuitId, sectionId: "s1", circuitListId: "l1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 } as never; } return null as never; }, async listBySection() { return [{ sortOrder: 30 }] as never[]; }, } as never, circuitSectionRepository: { async findById() { return { id: "s2", circuitListId: "l1", prefix: "-2F" } as never; }, } as never, numberingService: { async getNextIdentifier() { return "-2F8"; }, } as never, }); const result = await service.moveDeviceRowsBulk({ rowIds: ["r1", "r2"], targetSectionId: "s2", createNewCircuit: true, }); assert.equal(transactionCount, 1); assert.deepEqual(preparedTarget, { circuitListId: "l1", sectionId: "s2", equipmentIdentifier: "-2F8", displayName: "Neuer Stromkreis", sortOrder: 40, }); assert.equal(result.createdCircuitId, "c-new"); }); it("reorders circuits inside one section without renumbering identifiers", async () => { const updates: Array<{ id: string; sortOrder: number; equipmentIdentifier: string }> = []; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1" } as never; }, } as never, circuitRepository: { async listBySection() { return [ { id: "c1", sectionId: "s1", equipmentIdentifier: "-2F7", sortOrder: 10, isReserve: 0 }, { id: "c2", sectionId: "s1", equipmentIdentifier: "-2F9", sortOrder: 20, isReserve: 0 }, { id: "c3", sectionId: "s1", equipmentIdentifier: "-2F5", sortOrder: 30, isReserve: 1 }, ] as never[]; }, async update(id: string, payload: { sortOrder: number; equipmentIdentifier: string }) { updates.push({ id, sortOrder: payload.sortOrder, equipmentIdentifier: payload.equipmentIdentifier }); }, } as never, }); await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] }); assert.deepEqual(updates, [ { id: "c3", sortOrder: 10, equipmentIdentifier: "-2F5" }, { id: "c1", sortOrder: 20, equipmentIdentifier: "-2F7" }, { id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" }, ]); }); it("safe section identifier bulk update validates full section set (undo safety)", async () => { let safeCalled = false; const service = new CircuitWriteService({ circuitSectionRepository: { async findById() { return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never; }, } as never, circuitRepository: { async listBySection() { return [ { id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 }, { id: "c2", sectionId: "s1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 }, ] as never[]; }, async updateEquipmentIdentifiersSafely() { safeCalled = true; }, } as never, }); await service.updateSectionEquipmentIdentifiers("s1", { identifiers: [ { circuitId: "c1", equipmentIdentifier: "-1F2" }, { circuitId: "c2", equipmentIdentifier: "-1F1" }, ], }); assert.equal(safeCalled, true); }); it("safe identifier bulk update uses synchronous transaction callback", async () => { const repository = new CircuitRepository(); const originalTransaction = (db as unknown as { transaction: unknown }).transaction; let callbackReturnedPromise = false; (db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => { const fakeTx = { select() { return { from() { return { where() { return { all() { return [{ id: "c1" }, { id: "c2" }]; }, }; }, }; }, }; }, update() { return { set() { return { where() { return { run() { return; }, }; }, }; }, }; }, }; const result = cb(fakeTx); callbackReturnedPromise = Boolean(result && typeof (result as Promise).then === "function"); if (callbackReturnedPromise) { throw new Error("Transaction function cannot return a promise"); } }; try { await repository.updateEquipmentIdentifiersSafely( "l1", [ { id: "c1", equipmentIdentifier: "-1F1" }, { id: "c2", equipmentIdentifier: "-1F2" }, ], "s1" ); assert.equal(callbackReturnedPromise, false); } finally { (db as unknown as { transaction: unknown }).transaction = originalTransaction; } }); it("bulk device row move uses one synchronous transaction callback", () => { const repository = new CircuitDeviceRowRepository(); const originalTransaction = (db as unknown as { transaction: unknown }).transaction; let callbackReturnedPromise = false; let selectCall = 0; let rowUpdateCount = 0; const selectedRows = [ [ { id: "r1", circuitId: "c1" }, { id: "r2", circuitId: "c2" }, ], [{ id: "c3", circuitListId: "l1" }], [ { id: "c1", circuitListId: "l1" }, { id: "c2", circuitListId: "l1" }, ], [{ id: "existing-target-row", sortOrder: 10 }], [], [], ]; (db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => { const fakeTx = { select() { const rows = selectedRows[selectCall++] ?? []; const query = { from() { return query; }, where() { return query; }, limit() { return query; }, all() { return rows; }, }; return query; }, update() { return { set() { return { where() { return { run() { rowUpdateCount += 1; return { changes: 1 }; }, }; }, }; }, }; }, }; const callbackResult = cb(fakeTx); callbackReturnedPromise = Boolean( callbackResult && typeof (callbackResult as Promise).then === "function" ); if (callbackReturnedPromise) { throw new Error("Transaction function cannot return a promise"); } }; try { const result = repository.moveRowsTransactional({ rows: [ { id: "r1", expectedCircuitId: "c1" }, { id: "r2", expectedCircuitId: "c2" }, ], targetCircuitId: "c3", }); assert.equal(callbackReturnedPromise, false); assert.equal(rowUpdateCount, 5); assert.deepEqual(result, { movedRowIds: ["r1", "r2"], targetCircuitId: "c3", createdCircuitId: undefined, }); } finally { (db as unknown as { transaction: unknown }).transaction = originalTransaction; } }); it("device row creation and reserve activation share one synchronous transaction", () => { const repository = new CircuitDeviceRowRepository(); const originalTransaction = (db as unknown as { transaction: unknown }).transaction; let callbackReturnedPromise = false; let insertedValues: { id?: string; circuitId?: string; sortOrder?: number } | undefined; let reserveValue: number | undefined; let selectCall = 0; (db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => { const fakeTx = { select() { const rows = selectCall++ === 0 ? [{ id: "c1" }] : [{ sortOrder: 20 }]; const query = { from() { return query; }, where() { return query; }, limit() { return query; }, all() { return rows; }, }; return query; }, insert() { return { values(values: typeof insertedValues) { insertedValues = values; return { run() { return { changes: 1 }; }, }; }, }; }, update() { return { set(values: { isReserve: number }) { reserveValue = values.isReserve; return { where() { return { run() { return { changes: 1 }; }, }; }, }; }, }; }, }; const callbackResult = cb(fakeTx); callbackReturnedPromise = Boolean( callbackResult && typeof (callbackResult as Promise).then === "function" ); }; try { const rowId = repository.createInCircuitTransactional({ circuitId: "c1", name: "Load", displayName: "Load", quantity: 1, powerPerUnit: 1, simultaneityFactor: 1, }); assert.equal(callbackReturnedPromise, false); assert.equal(insertedValues?.id, rowId); assert.equal(insertedValues?.circuitId, "c1"); assert.equal(insertedValues?.sortOrder, 30); assert.equal(reserveValue, 0); } finally { (db as unknown as { transaction: unknown }).transaction = originalTransaction; } }); it("device row deletion and reserve update share one synchronous transaction", () => { const repository = new CircuitDeviceRowRepository(); const originalTransaction = (db as unknown as { transaction: unknown }).transaction; let callbackReturnedPromise = false; let deleteCount = 0; let reserveValue: number | undefined; let selectCall = 0; (db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => { const fakeTx = { select() { const rows = selectCall++ === 0 ? [{ id: "r1", circuitId: "c1" }] : []; const query = { from() { return query; }, where() { return query; }, limit() { return query; }, all() { return rows; }, }; return query; }, delete() { return { where() { return { run() { deleteCount += 1; return { changes: 1 }; }, }; }, }; }, update() { return { set(values: { isReserve: number }) { reserveValue = values.isReserve; return { where() { return { run() { return { changes: 1 }; }, }; }, }; }, }; }, }; const callbackResult = cb(fakeTx); callbackReturnedPromise = Boolean( callbackResult && typeof (callbackResult as Promise).then === "function" ); }; try { repository.deleteFromCircuitTransactional("r1", "c1"); assert.equal(callbackReturnedPromise, false); assert.equal(deleteCount, 1); assert.equal(reserveValue, 1); } finally { (db as unknown as { transaction: unknown }).transaction = originalTransaction; } }); it("tracks local edits on linked device rows as overridden fields", async () => { let savedOverrides: string | undefined; const current = { id: "r1", circuitId: "c1", linkedProjectDeviceId: "pd1", name: "Luminaire", displayName: "Local name", quantity: 1, powerPerUnit: 0.1, simultaneityFactor: 1, sortOrder: 10, overriddenFields: null, }; const service = new CircuitWriteService({ deviceRowRepository: { async findById() { return current as never; }, async update(_rowId: string, input: { overriddenFields?: string }) { savedOverrides = input.overriddenFields; }, } as never, }); await service.updateDeviceRow("r1", { quantity: 2 }); assert.deepEqual(JSON.parse(savedOverrides ?? "[]"), ["quantity"]); }); });