import { CircuitDeviceRowRepository } from "../../db/repositories/circuit-device-row.repository.js"; import { CircuitListRepository } from "../../db/repositories/circuit-list.repository.js"; import { CircuitRepository } from "../../db/repositories/circuit.repository.js"; import { CircuitSectionRepository } from "../../db/repositories/circuit-section.repository.js"; import { ProjectDeviceRepository } from "../../db/repositories/project-device.repository.js"; import type { CreateCircuitDeviceRowInput, CreateCircuitInput, CreateCircuitWithDeviceRowsInput, MoveCircuitDeviceRowInput, MoveCircuitDeviceRowsBulkInput, ReorderSectionCircuitsInput, UpdateSectionEquipmentIdentifiersInput, UpdateCircuitDeviceRowInput, UpdateCircuitInput, } from "../../shared/validation/circuit.schemas.js"; import { CircuitNumberingService } from "./circuit-numbering.service.js"; import { projectDeviceSyncFields } from "../../shared/constants/project-device-sync-fields.js"; import { parseOverriddenFields, serializeOverriddenFields, } from "./project-device-sync.service.js"; export class CircuitWriteService { private readonly circuitRepository: CircuitRepository; private readonly circuitSectionRepository: CircuitSectionRepository; private readonly circuitListRepository: CircuitListRepository; private readonly deviceRowRepository: CircuitDeviceRowRepository; private readonly projectDeviceRepository: ProjectDeviceRepository; private readonly numberingService: CircuitNumberingService; constructor(deps?: { circuitRepository?: CircuitRepository; circuitSectionRepository?: CircuitSectionRepository; circuitListRepository?: CircuitListRepository; deviceRowRepository?: CircuitDeviceRowRepository; projectDeviceRepository?: ProjectDeviceRepository; numberingService?: CircuitNumberingService; }) { this.circuitRepository = deps?.circuitRepository ?? new CircuitRepository(); this.circuitSectionRepository = deps?.circuitSectionRepository ?? new CircuitSectionRepository(); this.circuitListRepository = deps?.circuitListRepository ?? new CircuitListRepository(); this.deviceRowRepository = deps?.deviceRowRepository ?? new CircuitDeviceRowRepository(); this.projectDeviceRepository = deps?.projectDeviceRepository ?? new ProjectDeviceRepository(); this.numberingService = deps?.numberingService ?? new CircuitNumberingService(); } // Ensures writes never connect a section to the wrong circuit list. private async assertSectionInList(sectionId: string, circuitListId: string) { const section = await this.circuitSectionRepository.findById(sectionId); if (!section) { throw new Error("Invalid section id."); } if (section.circuitListId !== circuitListId) { throw new Error("Section does not belong to circuit list."); } return section; } // Enforces BMK uniqueness inside one circuit list. private async assertUniqueEquipmentIdentifier( circuitListId: string, equipmentIdentifier: string, excludeCircuitId?: string ) { const exists = await this.circuitRepository.existsByEquipmentIdentifier( circuitListId, equipmentIdentifier, excludeCircuitId ); if (exists) { throw new Error("Duplicate equipmentIdentifier in circuit list."); } } // Validates linked project-device id against owning project of the circuit list. private async assertValidLinkedProjectDeviceForProject( projectId: string, linkedProjectDeviceId?: string ) { if (!linkedProjectDeviceId) { return; } const device = await this.projectDeviceRepository.findById(projectId, linkedProjectDeviceId); if (!device) { throw new Error("Invalid linked project device id."); } } private async assertValidLinkedProjectDevice(circuitId: string, linkedProjectDeviceId?: string) { if (!linkedProjectDeviceId) { return; } const circuit = await this.circuitRepository.findById(circuitId); if (!circuit) { throw new Error("Invalid circuit id."); } const list = await this.circuitListRepository.findByIdByListIdOnly(circuit.circuitListId); if (!list) { throw new Error("Circuit list not found."); } await this.assertValidLinkedProjectDeviceForProject(list.projectId, linkedProjectDeviceId); } async createCircuit(projectId: string, circuitListId: string, input: CreateCircuitInput) { const list = await this.circuitListRepository.findById(projectId, circuitListId); if (!list) { throw new Error("Circuit list not found in project."); } await this.assertSectionInList(input.sectionId, circuitListId); await this.assertUniqueEquipmentIdentifier(circuitListId, input.equipmentIdentifier); const id = await this.circuitRepository.create({ circuitListId, sectionId: input.sectionId, equipmentIdentifier: input.equipmentIdentifier, displayName: input.displayName, sortOrder: input.sortOrder, protectionType: input.protectionType, protectionRatedCurrent: input.protectionRatedCurrent, protectionCharacteristic: input.protectionCharacteristic, cableType: input.cableType, cableCrossSection: input.cableCrossSection, cableLength: input.cableLength, rcdAssignment: input.rcdAssignment, terminalDesignation: input.terminalDesignation, voltage: input.voltage, controlRequirement: input.controlRequirement, status: input.status, isReserve: input.isReserve ?? true, remark: input.remark, }); return this.circuitRepository.findById(id); } async createCircuitWithDeviceRows( projectId: string, circuitListId: string, input: CreateCircuitWithDeviceRowsInput ) { const list = await this.circuitListRepository.findById(projectId, circuitListId); if (!list) { throw new Error("Circuit list not found in project."); } await this.assertSectionInList(input.circuit.sectionId, circuitListId); await this.assertUniqueEquipmentIdentifier( circuitListId, input.circuit.equipmentIdentifier ); for (const row of input.deviceRows) { await this.assertValidLinkedProjectDeviceForProject( list.projectId, row.linkedProjectDeviceId ); } const created = this.deviceRowRepository.createCircuitWithDeviceRowsTransactional({ circuit: { circuitListId, ...input.circuit, isReserve: false, }, deviceRows: input.deviceRows, }); const circuit = await this.circuitRepository.findById(created.circuitId); const deviceRows = await Promise.all( created.rowIds.map((rowId) => this.deviceRowRepository.findById(rowId)) ); if (!circuit || deviceRows.some((row) => !row)) { throw new Error("Der angelegte Stromkreis konnte nicht geladen werden."); } return { circuit, deviceRows, }; } async updateCircuit(circuitId: string, input: UpdateCircuitInput) { const current = await this.circuitRepository.findById(circuitId); if (!current) { throw new Error("Invalid circuit id."); } const sectionId = input.sectionId ?? current.sectionId; const equipmentIdentifier = input.equipmentIdentifier ?? current.equipmentIdentifier; const sortOrder = input.sortOrder ?? current.sortOrder; await this.assertSectionInList(sectionId, current.circuitListId); await this.assertUniqueEquipmentIdentifier(current.circuitListId, equipmentIdentifier, circuitId); await this.circuitRepository.update(circuitId, { sectionId, equipmentIdentifier, displayName: input.displayName ?? current.displayName ?? undefined, sortOrder, protectionType: input.protectionType ?? current.protectionType ?? undefined, protectionRatedCurrent: input.protectionRatedCurrent ?? current.protectionRatedCurrent ?? undefined, protectionCharacteristic: input.protectionCharacteristic ?? current.protectionCharacteristic ?? undefined, cableType: input.cableType ?? current.cableType ?? undefined, cableCrossSection: input.cableCrossSection ?? current.cableCrossSection ?? undefined, cableLength: input.cableLength ?? current.cableLength ?? undefined, rcdAssignment: input.rcdAssignment ?? current.rcdAssignment ?? undefined, terminalDesignation: input.terminalDesignation ?? current.terminalDesignation ?? undefined, voltage: input.voltage ?? current.voltage ?? undefined, controlRequirement: input.controlRequirement ?? current.controlRequirement ?? undefined, status: input.status ?? current.status ?? undefined, isReserve: input.isReserve ?? Boolean(current.isReserve), remark: input.remark ?? current.remark ?? undefined, }); return this.circuitRepository.findById(circuitId); } async deleteCircuit(circuitId: string) { const current = await this.circuitRepository.findById(circuitId); if (!current) { throw new Error("Invalid circuit id."); } await this.circuitRepository.delete(circuitId); } async createDeviceRow(circuitId: string, input: CreateCircuitDeviceRowInput) { const circuit = await this.circuitRepository.findById(circuitId); if (!circuit) { throw new Error("Invalid circuit id."); } await this.assertValidLinkedProjectDevice(circuitId, input.linkedProjectDeviceId); const rowId = this.deviceRowRepository.createInCircuitTransactional({ circuitId, linkedProjectDeviceId: input.linkedProjectDeviceId, sortOrder: input.sortOrder, name: input.name, displayName: input.displayName, phaseType: input.phaseType, connectionKind: input.connectionKind, costGroup: input.costGroup, category: input.category, level: input.level, roomId: input.roomId, roomNumberSnapshot: input.roomNumberSnapshot, roomNameSnapshot: input.roomNameSnapshot, quantity: input.quantity, powerPerUnit: input.powerPerUnit, simultaneityFactor: input.simultaneityFactor, cosPhi: input.cosPhi, remark: input.remark, overriddenFields: input.overriddenFields, }); return this.deviceRowRepository.findById(rowId); } async updateDeviceRow(rowId: string, input: UpdateCircuitDeviceRowInput) { const current = await this.deviceRowRepository.findById(rowId); if (!current) { throw new Error("Invalid device row id."); } await this.assertValidLinkedProjectDevice(current.circuitId, input.linkedProjectDeviceId); let overriddenFields = input.overriddenFields ?? current.overriddenFields ?? undefined; if (current.linkedProjectDeviceId && input.overriddenFields === undefined) { const overrides = new Set(parseOverriddenFields(current.overriddenFields)); const inputValues = input as Record; const currentValues = current as unknown as Record; for (const field of projectDeviceSyncFields) { if ( Object.prototype.hasOwnProperty.call(input, field) && (inputValues[field] ?? null) !== (currentValues[field] ?? null) ) { overrides.add(field); } } overriddenFields = serializeOverriddenFields(overrides); } await this.deviceRowRepository.update(rowId, { linkedProjectDeviceId: input.linkedProjectDeviceId ?? current.linkedProjectDeviceId ?? undefined, name: input.name ?? current.name, displayName: input.displayName ?? current.displayName, phaseType: input.phaseType ?? current.phaseType ?? undefined, connectionKind: input.connectionKind ?? current.connectionKind ?? undefined, costGroup: input.costGroup ?? current.costGroup ?? undefined, category: input.category ?? current.category ?? undefined, level: input.level ?? current.level ?? undefined, roomId: input.roomId ?? current.roomId ?? undefined, roomNumberSnapshot: input.roomNumberSnapshot ?? current.roomNumberSnapshot ?? undefined, roomNameSnapshot: input.roomNameSnapshot ?? current.roomNameSnapshot ?? undefined, quantity: input.quantity ?? current.quantity, powerPerUnit: input.powerPerUnit ?? current.powerPerUnit, simultaneityFactor: input.simultaneityFactor ?? current.simultaneityFactor, cosPhi: input.cosPhi ?? current.cosPhi ?? undefined, remark: input.remark ?? current.remark ?? undefined, overriddenFields, }); return this.deviceRowRepository.findById(rowId); } async deleteDeviceRow(rowId: string) { const current = await this.deviceRowRepository.findById(rowId); if (!current) { throw new Error("Invalid device row id."); } const circuit = await this.circuitRepository.findById(current.circuitId); if (!circuit) { throw new Error("Invalid circuit id."); } this.deviceRowRepository.deleteFromCircuitTransactional(rowId, circuit.id); } async moveDeviceRow(rowId: string, input: MoveCircuitDeviceRowInput) { const row = await this.deviceRowRepository.findById(rowId); if (!row) { throw new Error("Invalid device row id."); } const sourceCircuit = await this.circuitRepository.findById(row.circuitId); if (!sourceCircuit) { throw new Error("Invalid circuit id."); } let targetCircuit = input.targetCircuitId ? await this.circuitRepository.findById(input.targetCircuitId) : null; if (input.targetCircuitId && !targetCircuit) { throw new Error("Invalid target circuit id."); } let createTargetCircuit: | { circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName: string; sortOrder: number; } | undefined; // Placeholder-target move prepares a new circuit explicitly. Its creation and // the row assignment are committed together without renumbering other circuits. if (!targetCircuit) { if (!input.targetSectionId || !input.createNewCircuit) { throw new Error("Invalid move target."); } const section = await this.assertSectionInList(input.targetSectionId, sourceCircuit.circuitListId); const nextIdentifier = await this.numberingService.getNextIdentifier(section.id); const sectionCircuits = await this.circuitRepository.listBySection(section.id); const nextSortOrder = sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10; createTargetCircuit = { circuitListId: sourceCircuit.circuitListId, sectionId: section.id, equipmentIdentifier: nextIdentifier, displayName: "Neuer Stromkreis", sortOrder: nextSortOrder, }; } if (targetCircuit && targetCircuit.circuitListId !== sourceCircuit.circuitListId) { throw new Error("Target circuit does not belong to same circuit list."); } if (targetCircuit?.id === sourceCircuit.id) { return this.deviceRowRepository.findById(rowId); } this.deviceRowRepository.moveRowsTransactional({ rows: [{ id: row.id, expectedCircuitId: row.circuitId }], targetCircuitId: targetCircuit?.id, createTargetCircuit, }); return this.deviceRowRepository.findById(rowId); } async moveDeviceRowsBulk(input: MoveCircuitDeviceRowsBulkInput) { // Bulk move keeps input order and resolves all source circuits first so undo can // restore per-source assignment deterministically. const uniqueRowIds = [...new Set(input.rowIds)]; if (uniqueRowIds.length === 0) { throw new Error("No device rows provided."); } const rows = []; for (const rowId of uniqueRowIds) { const row = await this.deviceRowRepository.findById(rowId); if (!row) { throw new Error("Invalid device row id."); } rows.push(row); } const sourceCircuits = new Map>>(); for (const row of rows) { if (!sourceCircuits.has(row.circuitId)) { const circuit = await this.circuitRepository.findById(row.circuitId); if (!circuit) { throw new Error("Invalid circuit id."); } sourceCircuits.set(row.circuitId, circuit); } } const referenceSourceCircuit = sourceCircuits.get(rows[0].circuitId)!; let targetCircuit = input.targetCircuitId ? await this.circuitRepository.findById(input.targetCircuitId) : null; if (input.targetCircuitId && !targetCircuit) { throw new Error("Invalid target circuit id."); } // Bulk placeholder move prepares exactly one new circuit as common target. // Its actual creation happens together with the row moves in one transaction. let createTargetCircuit: | { circuitListId: string; sectionId: string; equipmentIdentifier: string; displayName: string; sortOrder: number; } | undefined; if (!targetCircuit) { if (!input.targetSectionId || !input.createNewCircuit) { throw new Error("Invalid move target."); } const section = await this.assertSectionInList(input.targetSectionId, referenceSourceCircuit.circuitListId); const nextIdentifier = await this.numberingService.getNextIdentifier(section.id); const sectionCircuits = await this.circuitRepository.listBySection(section.id); const nextSortOrder = sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10; createTargetCircuit = { circuitListId: referenceSourceCircuit.circuitListId, sectionId: section.id, equipmentIdentifier: nextIdentifier, displayName: "Neuer Stromkreis", sortOrder: nextSortOrder, }; } const targetCircuitListId = targetCircuit?.circuitListId ?? createTargetCircuit!.circuitListId; for (const sourceCircuit of sourceCircuits.values()) { if (sourceCircuit.circuitListId !== targetCircuitListId) { throw new Error("All moved rows must belong to same circuit list as target."); } } return this.deviceRowRepository.moveRowsTransactional({ rows: rows.map((row) => ({ id: row.id, expectedCircuitId: row.circuitId })), targetCircuitId: targetCircuit?.id, createTargetCircuit, }); } async getNextIdentifier(sectionId: string) { return this.numberingService.getNextIdentifier(sectionId); } async renumberSection(sectionId: string) { // Explicit renumber operation for one section only. // Never renumbers other sections and never runs implicitly during move/sort operations. const section = await this.circuitSectionRepository.findById(sectionId); if (!section) { throw new Error("Invalid section id."); } const sectionCircuits = await this.circuitRepository.listBySection(sectionId); const otherCircuits = (await this.circuitRepository.listByCircuitList(section.circuitListId)).filter( (circuit) => circuit.sectionId !== sectionId ); const otherIdentifiers = new Set(otherCircuits.map((circuit) => circuit.equipmentIdentifier)); const finalAssignments: Array<{ id: string; equipmentIdentifier: string }> = []; let index = 1; for (const circuit of sectionCircuits) { let candidate = `${section.prefix}${index}`; while (otherIdentifiers.has(candidate)) { index += 1; candidate = `${section.prefix}${index}`; } finalAssignments.push({ id: circuit.id, equipmentIdentifier: candidate }); index += 1; } // Uses safe two-phase identifier update to avoid UNIQUE collisions during swaps. await this.circuitRepository.updateEquipmentIdentifiersSafely( section.circuitListId, finalAssignments, sectionId ); return this.circuitRepository.listBySection(sectionId); } async updateSectionEquipmentIdentifiers( sectionId: string, input: UpdateSectionEquipmentIdentifiersInput ) { const section = await this.circuitSectionRepository.findById(sectionId); if (!section) { throw new Error("Invalid section id."); } const sectionCircuits = await this.circuitRepository.listBySection(sectionId); const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id)); if (input.identifiers.length !== sectionCircuits.length) { throw new Error("identifiers must include all circuits in the section."); } for (const entry of input.identifiers) { if (!sectionIds.has(entry.circuitId)) { throw new Error("Circuit id does not belong to section."); } } await this.circuitRepository.updateEquipmentIdentifiersSafely( section.circuitListId, input.identifiers.map((entry) => ({ id: entry.circuitId, equipmentIdentifier: entry.equipmentIdentifier })), sectionId ); return this.circuitRepository.listBySection(sectionId); } async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) { // Reorder updates sortOrder only. BMKs remain unchanged; users may renumber explicitly later. const section = await this.circuitSectionRepository.findById(sectionId); if (!section) { throw new Error("Invalid section id."); } const sectionCircuits = await this.circuitRepository.listBySection(sectionId); const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id)); if (sectionCircuits.length !== input.orderedCircuitIds.length) { throw new Error("orderedCircuitIds must include all circuits of the section."); } for (const circuitId of input.orderedCircuitIds) { if (!sectionIds.has(circuitId)) { throw new Error("Circuit id does not belong to section."); } } this.circuitRepository.updateSortOrdersSafely(sectionId, input.orderedCircuitIds); return this.circuitRepository.listBySection(sectionId); } }