import { and, eq, inArray } from "drizzle-orm" import { FastifyInstance } from "fastify" import { costcentres, createddocuments } from "../../db/schema" import { insertHistoryItem } from "../utils/history" export type RowCostCentreAssignment = { rowId: string costcentre: string | null } export type OutgoingDocumentCostCentreUpdate = { costcentre?: string | null rowCostCentres?: RowCostCentreAssignment[] } const serviceError = (message: string, statusCode = 400) => Object.assign(new Error(message), { statusCode }) const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i export function applyRowCostCentreAssignments( rows: unknown, assignments: RowCostCentreAssignment[] = [], ) { if (!Array.isArray(rows)) return [] if (!assignments.length) return rows const assignmentMap = new Map(assignments.map((assignment) => [String(assignment.rowId), assignment.costcentre])) const existingIds = new Set(rows .filter((row) => row && typeof row === "object" && !Array.isArray(row)) .map((row) => (row as Record).id) .filter((id) => id !== null && id !== undefined && id !== "") .map(String)) for (const rowId of assignmentMap.keys()) { if (!existingIds.has(rowId)) throw serviceError(`Position ${rowId} wurde im Ausgangsbeleg nicht gefunden`) } return rows.map((row) => { if (!row || typeof row !== "object" || Array.isArray(row)) return row const rowRecord = row as Record if (rowRecord.id === null || rowRecord.id === undefined || rowRecord.id === "") return row const rowId = String(rowRecord.id) if (!assignmentMap.has(rowId)) return row const { costcentre: _legacyCostCentre, ...rest } = rowRecord return { ...rest, costCentre: assignmentMap.get(rowId) ?? null } }) } export async function updateOutgoingDocumentCostCentres( server: FastifyInstance, tenantId: number, userId: string, documentId: number, input: OutgoingDocumentCostCentreUpdate, ) { const hasDocumentCostCentre = Object.prototype.hasOwnProperty.call(input, "costcentre") if (hasDocumentCostCentre && input.costcentre !== null && !UUID_PATTERN.test(String(input.costcentre))) { throw serviceError("costcentre muss eine gültige UUID oder null sein") } if (input.rowCostCentres !== undefined && !Array.isArray(input.rowCostCentres)) { throw serviceError("rowCostCentres muss ein Array sein") } const rowAssignments = (input.rowCostCentres || []).map((assignment) => { const rowId = typeof assignment?.rowId === "string" ? assignment.rowId.trim() : "" const costcentre = assignment?.costcentre if (!rowId) throw serviceError("Jede Positionszuordnung benötigt eine rowId") if (costcentre !== null && !UUID_PATTERN.test(String(costcentre))) { throw serviceError(`Ungültige Kostenstelle für Position ${rowId}`) } return { rowId, costcentre } }) if (!hasDocumentCostCentre && !rowAssignments.length) { throw serviceError("Mindestens eine Beleg- oder Positionskostenstelle ist erforderlich") } const [existing] = await server.db .select() .from(createddocuments) .where(and( eq(createddocuments.id, documentId), eq(createddocuments.tenant, tenantId), eq(createddocuments.archived, false), )) .limit(1) if (!existing) throw serviceError("Ausgangsbeleg nicht gefunden", 404) if (existing.state !== "Gebucht") { throw serviceError("Kostenstellen können über diesen Weg nur bei fertiggestellten Ausgangsbelegen geändert werden") } const referencedCostCentreIds = [...new Set([ ...(hasDocumentCostCentre && input.costcentre ? [input.costcentre] : []), ...rowAssignments.map((assignment) => assignment.costcentre).filter((id): id is string => Boolean(id)), ])] if (referencedCostCentreIds.length) { const validCostCentres = await server.db .select({ id: costcentres.id }) .from(costcentres) .where(and( eq(costcentres.tenant, tenantId), eq(costcentres.archived, false), inArray(costcentres.id, referencedCostCentreIds), )) if (validCostCentres.length !== referencedCostCentreIds.length) { throw serviceError("Mindestens eine Kostenstelle wurde nicht gefunden oder ist archiviert") } } const updatedRows = applyRowCostCentreAssignments(existing.rows, rowAssignments) const update: Record = { rows: updatedRows, updatedAt: new Date(), updatedBy: userId, } if (hasDocumentCostCentre) update.costcentre = input.costcentre ?? null const [updated] = await server.db .update(createddocuments) .set(update) .where(and( eq(createddocuments.id, documentId), eq(createddocuments.tenant, tenantId), eq(createddocuments.archived, false), eq(createddocuments.state, "Gebucht"), )) .returning() if (!updated) throw serviceError("Ausgangsbeleg nicht gefunden", 404) await insertHistoryItem(server, { tenant_id: tenantId, created_by: userId, entity: "createddocuments", entityId: documentId, action: "updated", oldVal: existing, newVal: updated, text: "Kostenstellen des fertiggestellten Ausgangsbelegs geändert", }) return updated }