176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
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.")
|
||
|
||
}
|
||
}
|
||
|
||
}
|