147 lines
5.5 KiB
TypeScript
147 lines
5.5 KiB
TypeScript
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<string, unknown>).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<string, unknown>
|
|
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<string, unknown> = {
|
|
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
|
|
}
|