feat(mcp): Belegvorbereitung für Eingangsrechnungen anstoßen

- accounting.incoming_invoices.prepare startet die Vorbereitung für alle offenen oder ausgewählte Dateien und liefert angelegte Entwürfe samt Validierung sowie übersprungene Dateien
- Vorbereitungslauf meldet sein Ergebnis und läuft je Mandant nacheinander, damit parallele Aufrufe keine doppelten Belege anlegen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 16:03:36 +01:00
parent 0777315d1d
commit c6fd53307c
3 changed files with 137 additions and 11 deletions

View File

@@ -17,7 +17,7 @@ import {
incominginvoices,
} from "../../../db/schema"
import { eq, and, isNull, not } from "drizzle-orm"
import { eq, and, inArray, isNull, not } from "drizzle-orm"
const formatInvoiceItemDescription = (item: any) => {
const parts = [
@@ -86,8 +86,16 @@ const mapGptDataToIncomingInvoice = (data: any, tenantId: number) => {
return itemInfo
}
export type IncomingInvoicePreparationResult = {
filesFound: number
created: Array<{ fileId: string, invoiceId: number, source: string | null }>
skipped: Array<{ fileId: string, reason: string }>
error?: string
}
export function prepareIncomingInvoices(server: FastifyInstance) {
const processInvoices = async (tenantId:number) => {
const processInvoices = async (tenantId:number, fileIds?: string[]) => {
const result: IncomingInvoicePreparationResult = { filesFound: 0, created: [], skipped: [] }
console.log("▶ Starting Incoming Invoice Preparation")
const tenantsRes = await server.db
@@ -98,7 +106,7 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
if (!tenantsRes.length) {
console.log("No tenants with autoPrepareIncomingInvoices = true")
return
return { ...result, error: "Mandant nicht gefunden" }
}
console.log(`Processing tenants: ${tenantsRes.map(t => t.id).join(", ")}`)
@@ -124,6 +132,7 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
const invoiceFileTag = tagRes?.[0]?.id
if (!invoiceFileTag) {
server.log.error(`❌ Missing filetag 'invoices' for tenant ${tenantId}`)
result.error = "Dateityp für Eingangsrechnungen fehlt"
continue
}
@@ -137,10 +146,13 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
eq(files.type, invoiceFileTag),
isNull(files.incominginvoice),
eq(files.archived, false),
not(isNull(files.path))
not(isNull(files.path)),
...(fileIds ? [inArray(files.id, fileIds)] : [])
)
)
result.filesFound += filesRes.length
if (!filesRes.length) {
console.log(`No invoice files for tenant ${tenantId}`)
continue
@@ -158,6 +170,7 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
fileData = await loadFileBuffer(file.path!)
} catch (error) {
server.log.error(error, `Datei ${file.id} konnte nicht aus S3 geladen werden.`)
result.skipped.push({ fileId: file.id, reason: "Datei konnte nicht geladen werden" })
continue
}
@@ -186,7 +199,10 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
} catch (error) {
server.log.error(error, `E-Rechnung aus Datei ${file.id} konnte nicht verarbeitet werden.`)
if (electronicInvoice.container === "xml") continue
if (electronicInvoice.container === "xml") {
result.skipped.push({ fileId: file.id, reason: "E-Rechnung konnte nicht verarbeitet werden" })
continue
}
}
}
@@ -195,6 +211,7 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
if (!data) {
server.log.warn(`GPT returned no data for file ${file.id}`)
result.skipped.push({ fileId: file.id, reason: "Keine Belegdaten erkannt" })
continue
}
@@ -213,6 +230,7 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
if (!newInvoice) {
server.log.error(`Failed to insert incoming invoice for file ${file.id}`)
result.skipped.push({ fileId: file.id, reason: "Eingangsbeleg konnte nicht angelegt werden" })
continue
}
@@ -224,20 +242,31 @@ export function prepareIncomingInvoices(server: FastifyInstance) {
.set({ incominginvoice: newInvoice.id })
.where(eq(files.id, file.id))
result.created.push({ fileId: file.id, invoiceId: newInvoice.id, source: newInvoice.preparationSource ?? null })
console.log(`IncomingInvoice ${newInvoice.id} created for file ${file.id}`)
}
}
return
return result
}
return {
run: async (tenant:number) => {
await processInvoices(tenant)
console.log("Incoming Invoice Preparation Completed.")
// Serialize runs per tenant so concurrent triggers cannot prepare the same file twice.
const running = new Map<number, Promise<unknown>>()
return {
run: async (tenant:number, options: { fileIds?: string[] } = {}) => {
const previous = running.get(tenant) || Promise.resolve()
const current = previous.catch(() => undefined).then(() => processInvoices(tenant, options.fileIds))
running.set(tenant, current)
try {
const result = await current
console.log("Incoming Invoice Preparation Completed.")
return result
} finally {
if (running.get(tenant) === current) running.delete(tenant)
}
}
}