From 8a8711327595a8ea05064b1810e15fcaa3f89cad Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Fri, 2 Jan 2026 12:44:26 +0100 Subject: [PATCH] Added Prepare Service --- src/modules/cron/prepareIncomingInvoices.ts | 175 ++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/modules/cron/prepareIncomingInvoices.ts diff --git a/src/modules/cron/prepareIncomingInvoices.ts b/src/modules/cron/prepareIncomingInvoices.ts new file mode 100644 index 0000000..011c1a3 --- /dev/null +++ b/src/modules/cron/prepareIncomingInvoices.ts @@ -0,0 +1,175 @@ +import { FastifyInstance } from "fastify" +import dayjs from "dayjs" +import { getInvoiceDataFromGPT } from "../../utils/gpt" + +// Drizzle schema +import { + tenants, + files, + filetags, + incominginvoices, +} from "../../../db/schema" + +import { eq, and, isNull, not } from "drizzle-orm" + +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️⃣ Jede Datei einzeln durch GPT jagen & IncomingInvoice erzeugen + // ------------------------------------------------------------- + for (const file of filesRes) { + console.log(`Processing file ${file.id} for tenant ${tenantId}`) + + const data = await getInvoiceDataFromGPT(server,file, tenantId) + + if (!data) { + server.log.warn(`GPT returned no data for file ${file.id}`) + continue + } + + // --------------------------------------------------------- + // 3.1 IncomingInvoice-Objekt vorbereiten + // --------------------------------------------------------- + let itemInfo: any = { + tenant: tenantId, + state: "Vorbereitet" + } + + if (data.invoice_number) itemInfo.reference = data.invoice_number + if (data.invoice_date) itemInfo.date = dayjs(data.invoice_date).toISOString() + if (data.issuer?.id) itemInfo.vendor = data.issuer.id + if (data.invoice_duedate) itemInfo.dueDate = dayjs(data.invoice_duedate).toISOString() + + // Payment terms mapping + const mapPayment: any = { + "Direct Debit": "Einzug", + "Transfer": "Überweisung", + "Credit Card": "Kreditkarte", + "Other": "Sonstiges", + } + if (data.terms) itemInfo.paymentType = mapPayment[data.terms] ?? data.terms + + // 3.2 Positionszeilen konvertieren + if (data.invoice_items?.length > 0) { + itemInfo.accounts = data.invoice_items.map(item => ({ + account: item.account_id, + description: item.description, + amountNet: item.total_without_tax, + amountTax: Number((item.total - item.total_without_tax).toFixed(2)), + taxType: String(item.tax_rate), + amountGross: item.total, + costCentre: null, + quantity: item.quantity, + })) + } + + // 3.3 Beschreibung generieren + let description = "" + if (data.delivery_note_number) description += `Lieferschein: ${data.delivery_note_number}\n` + if (data.reference) description += `Referenz: ${data.reference}\n` + if (data.invoice_items) { + for (const item of data.invoice_items) { + description += `${item.description} - ${item.quantity} ${item.unit} - ${item.total}\n` + } + } + itemInfo.description = description.trim() + + // --------------------------------------------------------- + // 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.") + + } + } + +}