Files
FEDEO/backend/src/modules/cron/prepareIncomingInvoices.ts
2026-01-06 12:07:43 +01:00

176 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.")
}
}
}