import { FastifyInstance } from "fastify" import dayjs from "dayjs" import { getInvoiceDataFromGPT } from "../../utils/gpt" import { loadFileBuffer } from "../../utils/fileBuffer" import { detectElectronicInvoice } from "../einvoice/detectElectronicInvoice" import { parseElectronicInvoice } from "../einvoice/parseElectronicInvoice" import { findElectronicInvoiceVendor, mapElectronicInvoiceToIncomingInvoice, } from "../einvoice/mapElectronicInvoice" // Drizzle schema import { tenants, files, filetags, incominginvoices, } from "../../../db/schema" import { eq, and, isNull, not } from "drizzle-orm" const formatInvoiceItemDescription = (item: any) => { const parts = [ typeof item.description === "string" ? item.description.trim() : "", item.quantity !== null && item.quantity !== undefined ? [item.quantity, item.unit].filter(Boolean).join(" ") : (typeof item.unit === "string" ? item.unit.trim() : ""), typeof item.total === "number" ? `${item.total.toFixed(2)} EUR` : "", ].filter(Boolean) return parts.join(" - ") } const mapGptDataToIncomingInvoice = (data: any, tenantId: number) => { const itemInfo: any = { tenant: tenantId, state: "Vorbereitet", preparationSource: "gpt", } if (data.invoice_number) itemInfo.reference = data.invoice_number if (data.invoice_date && dayjs(data.invoice_date).isValid()) itemInfo.date = dayjs(data.invoice_date).toISOString() if (data.issuer?.id) itemInfo.vendor = data.issuer.id if (data.invoice_duedate && dayjs(data.invoice_duedate).isValid()) itemInfo.dueDate = dayjs(data.invoice_duedate).toISOString() const mapPayment: Record = { "Direct Debit": "Einzug", "Transfer": "Überweisung", "Credit Card": "Kreditkarte", "Other": "Sonstiges", } if (data.terms) itemInfo.paymentType = mapPayment[data.terms] ?? data.terms if (data.invoice_items?.length > 0) { itemInfo.accounts = data.invoice_items .filter((item: any) => item.description || item.total !== null || item.total_without_tax !== null) .map((item: any) => { const total = typeof item.total === "number" ? item.total : null const totalWithoutTax = typeof item.total_without_tax === "number" ? item.total_without_tax : null const amountTax = total !== null && totalWithoutTax !== null ? Number((total - totalWithoutTax).toFixed(2)) : null return { account: item.account_id, description: item.description, amountNet: totalWithoutTax, amountTax, taxType: item.tax_rate !== null ? String(item.tax_rate) : null, amountGross: total, costCentre: null, quantity: item.quantity, } }) } let description = "" if (data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number}\n` if (data.reference) description += `Referenz: ${data.reference}\n` for (const item of data.invoice_items || []) { const line = formatInvoiceItemDescription(item) if (line) description += `${line}\n` } itemInfo.description = description.trim() return itemInfo } export function prepareIncomingInvoices(server: FastifyInstance) { const processInvoices = async (tenantId:number) => { console.log("▶ Starting Incoming Invoice Preparation") const tenantsRes = await server.db .select() .from(tenants) .where(eq(tenants.id, tenantId)) .orderBy(tenants.id) if (!tenantsRes.length) { console.log("No tenants with autoPrepareIncomingInvoices = true") return } console.log(`Processing tenants: ${tenantsRes.map(t => t.id).join(", ")}`) // ------------------------------------------------------------- // 2️⃣ Jeden Tenant einzeln verarbeiten // ------------------------------------------------------------- for (const tenant of tenantsRes) { const tenantId = tenant.id // 2.1 Datei-Tags holen für incoming invoices const tagRes = await server.db .select() .from(filetags) .where( and( eq(filetags.tenant, tenantId), eq(filetags.incomingDocumentType, "invoices") ) ) .limit(1) const invoiceFileTag = tagRes?.[0]?.id if (!invoiceFileTag) { server.log.error(`❌ Missing filetag 'invoices' for tenant ${tenantId}`) continue } // 2.2 Alle Dateien laden, die als Invoice markiert sind aber NOCH keine incominginvoice haben const filesRes = await server.db .select() .from(files) .where( and( eq(files.tenant, tenantId), eq(files.type, invoiceFileTag), isNull(files.incominginvoice), eq(files.archived, false), not(isNull(files.path)) ) ) if (!filesRes.length) { console.log(`No invoice files for tenant ${tenantId}`) continue } // ------------------------------------------------------------- // 3️⃣ Strukturierte E-Rechnung bevorzugen, GPT als PDF-Fallback verwenden // ------------------------------------------------------------- for (const file of filesRes) { console.log(`Processing file ${file.id} for tenant ${tenantId}`) let fileData: Buffer try { fileData = await loadFileBuffer(file.path!) } catch (error) { server.log.error(error, `Datei ${file.id} konnte nicht aus S3 geladen werden.`) continue } const electronicInvoice = await detectElectronicInvoice(fileData, file) let itemInfo: any = null if (electronicInvoice) { server.log.info({ fileId: file.id, container: electronicInvoice.container, syntax: electronicInvoice.syntax, attachmentName: electronicInvoice.attachmentName, }, "Strukturierte E-Rechnung erkannt.") try { const parsedInvoice = parseElectronicInvoice(electronicInvoice.xml, electronicInvoice.syntax) const vendor = await findElectronicInvoiceVendor(server, tenantId, parsedInvoice) itemInfo = mapElectronicInvoiceToIncomingInvoice( parsedInvoice, tenantId, vendor?.id || null, electronicInvoice.container, electronicInvoice.attachmentName, ) } catch (error) { server.log.error(error, `E-Rechnung aus Datei ${file.id} konnte nicht verarbeitet werden.`) if (electronicInvoice.container === "xml") continue } } if (!itemInfo) { const data = await getInvoiceDataFromGPT(server, file, tenantId, fileData) if (!data) { server.log.warn(`GPT returned no data for file ${file.id}`) continue } itemInfo = mapGptDataToIncomingInvoice(data, tenantId) } // --------------------------------------------------------- // 4️⃣ IncomingInvoice erstellen // --------------------------------------------------------- const inserted = await server.db .insert(incominginvoices) .values(itemInfo) .returning() const newInvoice = inserted?.[0] if (!newInvoice) { server.log.error(`Failed to insert incoming invoice for file ${file.id}`) continue } // --------------------------------------------------------- // 5️⃣ Datei mit incominginvoice-ID verbinden // --------------------------------------------------------- await server.db .update(files) .set({ incominginvoice: newInvoice.id }) .where(eq(files.id, file.id)) console.log(`IncomingInvoice ${newInvoice.id} created for file ${file.id}`) } } return } return { run: async (tenant:number) => { await processInvoices(tenant) console.log("Incoming Invoice Preparation Completed.") } } }