import crypto from "node:crypto"; import type { Request, Response } from "express"; import { assertExternalCsvConfiguration } from "../../external-model/csv/external-csv-configuration.js"; import { ExternalCsvParseError } from "../../external-model/csv/external-csv-transport.js"; import { createExternalCsvPreview } from "../../external-model/application/external-csv-preview.js"; import { createExternalInitialImportPlan } from "../../external-model/application/external-initial-import-plan.js"; import { buildExternalInitialImportTarget } from "../../external-model/application/external-initial-import-target.js"; import { parseExternalCsv } from "../../external-model/csv/external-csv-transport.js"; import { createEmptyExternalModelState, createExternalInitialImportProjectCommand, } from "../../domain/models/external-initial-import-project-command.model.js"; import { createExternalCsvConfigurationUpdateProjectCommand } from "../../domain/models/external-csv-configuration-project-command.model.js"; import { previewExternalCsvSchema, planExternalInitialImportSchema, applyExternalInitialImportSchema, updateExternalCsvConfigurationSchema, } from "../../shared/validation/external-csv.schemas.js"; import { distributionBoardRepository, externalCsvConfigurationRepository, externalModelStateRepository, floorRepository, projectDeviceRepository, roomRepository, } from "../composition/application-repositories.js"; import { projectCommandService } from "../composition/project-command-stores.js"; import { respondWithProjectCommandError } from "./project-command.controller.js"; export function getExternalCsvConfiguration(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; const result = externalCsvConfigurationRepository.getByProject(projectId); if (!result.projectExists) { return res.status(404).json({ error: "Project not found" }); } return res.json({ configuration: result.configuration }); } export function updateExternalCsvConfiguration(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; const parsed = updateExternalCsvConfigurationSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } try { assertExternalCsvConfiguration(parsed.data.configuration); const current = externalCsvConfigurationRepository.getByProject(projectId); if (!current.projectExists) { return res.status(404).json({ error: "Project not found" }); } const target = { id: current.configuration?.id ?? crypto.randomUUID(), projectId, configurationVersion: (current.configuration?.configurationVersion ?? 0) + 1, configuration: parsed.data.configuration, }; const result = projectCommandService.executeUser({ projectId, expectedRevision: parsed.data.expectedRevision, description: "Revit-CSV-Konfiguration bearbeiten", command: createExternalCsvConfigurationUpdateProjectCommand( current.configuration, target ), }); return res.json({ ...result, configuration: target }); } catch (error) { return respondWithProjectCommandError(error, res); } } export function previewExternalCsv(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; const parsed = previewExternalCsvSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } const stored = externalCsvConfigurationRepository.getByProject(projectId); if (!stored.projectExists) { return res.status(404).json({ error: "Project not found" }); } if (!stored.configuration) { return res.status(409).json({ error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.", code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED", }); } try { const bytes = decodeBase64(parsed.data.contentBase64); return res.json( createExternalCsvPreview({ fileName: parsed.data.fileName, bytes, configuration: stored.configuration.configuration, }) ); } catch (error) { if (error instanceof ExternalCsvParseError) { return res.status(400).json({ error: error.message, code: error.code }); } return res.status(400).json({ error: error instanceof Error ? error.message : "CSV-Vorschau fehlgeschlagen.", }); } } export async function planExternalInitialImport(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; const parsed = planExternalInitialImportSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } const stored = externalCsvConfigurationRepository.getByProject(projectId); if (!stored.projectExists) { return res.status(404).json({ error: "Project not found" }); } if (!stored.configuration) { return res.status(409).json({ error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.", code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED", }); } const externalState = externalModelStateRepository.getByProject(projectId); if (externalState.state.source !== null) { return res.status(409).json({ error: "Für dieses Projekt wurde bereits ein Revit-Modell importiert.", code: "EXTERNAL_INITIAL_IMPORT_ALREADY_APPLIED", }); } try { const bytes = decodeBase64(parsed.data.contentBase64); const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([ floorRepository.listByProject(projectId), roomRepository.listByProject(projectId), distributionBoardRepository.listByProject(projectId), projectDeviceRepository.listByProject(projectId), ]); return res.json(createExternalInitialImportPlan({ fileName: parsed.data.fileName, bytes, configurationVersion: stored.configuration.configurationVersion, configuration: stored.configuration.configuration, floors, rooms, distributionBoards, projectDevices, })); } catch (error) { if (error instanceof ExternalCsvParseError) { return res.status(400).json({ error: error.message, code: error.code }); } return res.status(400).json({ error: error instanceof Error ? error.message : "Erstimport-Planung fehlgeschlagen.", }); } } export async function applyExternalInitialImport(req: Request, res: Response) { const projectId = getProjectId(req, res); if (!projectId) return; const parsed = applyExternalInitialImportSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: parsed.error.flatten() }); } const stored = externalCsvConfigurationRepository.getByProject(projectId); if (!stored.projectExists) { return res.status(404).json({ error: "Project not found" }); } if (!stored.configuration) { return res.status(409).json({ error: "Für das Projekt ist noch keine Revit-CSV-Konfiguration gespeichert.", code: "EXTERNAL_CSV_CONFIGURATION_REQUIRED", }); } if ( stored.configuration.configurationVersion !== parsed.data.expectedConfigurationVersion ) { return res.status(409).json({ error: "Die Revit-CSV-Konfiguration wurde seit der Planung geändert.", code: "EXTERNAL_CSV_CONFIGURATION_CHANGED", }); } try { const bytes = decodeBase64(parsed.data.contentBase64); const [floors, rooms, distributionBoards, projectDevices] = await Promise.all([ floorRepository.listByProject(projectId), roomRepository.listByProject(projectId), distributionBoardRepository.listByProject(projectId), projectDeviceRepository.listByProject(projectId), ]); const plan = createExternalInitialImportPlan({ fileName: parsed.data.fileName, bytes, configurationVersion: stored.configuration.configurationVersion, configuration: stored.configuration.configuration, floors, rooms, distributionBoards, projectDevices, }); if (plan.sha256 !== parsed.data.expectedSha256) { return res.status(409).json({ error: "Die CSV-Datei stimmt nicht mehr mit der geprüften Planung überein.", code: "EXTERNAL_CSV_FILE_CHANGED", }); } const target = buildExternalInitialImportTarget({ projectId, expectedRevision: parsed.data.expectedRevision, sourceName: parsed.data.sourceName, importedAtIso: new Date().toISOString(), configurationVersion: stored.configuration.configurationVersion, configuration: stored.configuration.configuration, originalContentBase64: parsed.data.contentBase64, document: parseExternalCsv(bytes, stored.configuration.configuration), plan, roomDecisions: parsed.data.roomDecisions, familyProjectDeviceDecisions: parsed.data.familyProjectDeviceDecisions, createId: () => crypto.randomUUID(), }); const result = projectCommandService.executeUser({ projectId, expectedRevision: parsed.data.expectedRevision, description: `Revit-Erstimport ${parsed.data.fileName}`, command: createExternalInitialImportProjectCommand( createEmptyExternalModelState(), target ), }); return res.status(201).json({ ...result, import: { sourceId: target.source!.id, importBatchId: target.importBatches[0].id, objectCount: target.objects.length, }, }); } catch (error) { if (error instanceof ExternalCsvParseError) { return res.status(400).json({ error: error.message, code: error.code }); } return respondWithProjectCommandError(error, res); } } function decodeBase64(value: string) { if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value) || value.length % 4 !== 0) { throw new Error("CSV-Inhalt ist nicht gültig Base64-kodiert."); } const bytes = Buffer.from(value, "base64"); if (bytes.toString("base64") !== value) { throw new Error("CSV-Inhalt ist nicht kanonisch Base64-kodiert."); } if (bytes.length > 18 * 1024 * 1024) { throw new Error("CSV-Datei überschreitet die maximale Größe von 18 MB."); } return bytes; } function getProjectId(req: Request, res: Response) { const { projectId } = req.params; if (typeof projectId !== "string" || !projectId.trim()) { res.status(400).json({ error: "Invalid projectId" }); return null; } return projectId; }