Added Backend

This commit is contained in:
2026-01-06 12:07:43 +01:00
parent b013ef8f4b
commit 6f3d4c0bff
165 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import crypto from "crypto";
import {secrets} from "./secrets"
const ALGORITHM = "aes-256-gcm";
export function encrypt(text) {
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
iv: iv.toString("hex"),
content: encrypted.toString("hex"),
tag: tag.toString("hex"),
};
}
export function decrypt({ iv, content, tag }) {
const ENCRYPTION_KEY = Buffer.from(secrets.ENCRYPTION_KEY, "hex");
const decipher = crypto.createDecipheriv(
ALGORITHM,
ENCRYPTION_KEY,
Buffer.from(iv, "hex")
);
decipher.setAuthTag(Buffer.from(tag, "hex"));
const decrypted = Buffer.concat([
decipher.update(Buffer.from(content, "hex")),
decipher.final(),
]);
return decrypted.toString("utf8");
}

View File

@@ -0,0 +1,27 @@
import { ilike, or } from "drizzle-orm"
/**
* Erzeugt eine OR-Suchbedingung über mehrere Spalten
*
* @param table - Drizzle Table Schema
* @param columns - Array der Spaltennamen (property names im schema)
* @param search - Suchbegriff
*/
export function buildSearchWhere(table: any, columns: string[], search: string) {
if (!search || !columns.length) return undefined
const term = `%${search.toLowerCase()}%`
const parts = columns
.map((colName) => {
const col = table[colName]
if (!col) return null
return ilike(col, term)
})
.filter(Boolean)
if (parts.length === 0) return undefined
// @ts-ignore
return or(...parts)
}

103
backend/src/utils/diff.ts Normal file
View File

@@ -0,0 +1,103 @@
import {diffTranslations} from "./diffTranslations";
export type DiffChange = {
key: string;
label: string;
oldValue: any;
newValue: any;
type: "created" | "updated" | "deleted" | "unchanged";
typeLabel: "erstellt" | "geändert" | "gelöscht" | "unverändert";
};
const IGNORED_KEYS = new Set([
"updated_at",
"updated_by",
"created_at",
"created_by",
"id",
"phases"
]);
/**
* Vergleicht zwei Objekte und gibt die Änderungen zurück.
* @param obj1 Altes Objekt
* @param obj2 Neues Objekt
* @param ctx Lookup-Objekte (z. B. { projects, customers, vendors, profiles, plants })
*/
export function diffObjects(
obj1: Record<string, any>,
obj2: Record<string, any>,
ctx: Record<string, any> = {}
): DiffChange[] {
const diffs: DiffChange[] = [];
const allKeys = new Set([
...Object.keys(obj1 || {}),
...Object.keys(obj2 || {}),
]);
for (const key of allKeys) {
if (IGNORED_KEYS.has(key)) continue; // Felder überspringen
const oldVal = obj1?.[key];
const newVal = obj2?.[key];
console.log(oldVal, key, newVal);
// Wenn beides null/undefined → ignorieren
if (
(oldVal === null || oldVal === undefined || oldVal === "" || JSON.stringify(oldVal) === "[]") &&
(newVal === null || newVal === undefined || newVal === "" || JSON.stringify(newVal) === "[]")
) {
continue;
}
let type: DiffChange["type"] = "unchanged";
let typeLabel: DiffChange["typeLabel"] = "unverändert";
if (oldVal === newVal) {
type = "unchanged";
typeLabel = "unverändert";
} else if (oldVal === undefined) {
type = "created";
typeLabel = "erstellt"
} else if (newVal === undefined) {
type = "deleted";
typeLabel = "gelöscht"
} else {
type = "updated";
typeLabel = "geändert"
}
if (type === "unchanged") continue;
const translation = diffTranslations[key];
let label = key;
let resolvedOld = oldVal;
let resolvedNew = newVal;
if (translation) {
label = translation.label;
if (translation.resolve) {
const { oldVal: resOld, newVal: resNew } = translation.resolve(
oldVal,
newVal,
ctx
);
resolvedOld = resOld;
resolvedNew = resNew;
}
}
diffs.push({
key,
label,
typeLabel,
oldValue: resolvedOld ?? "-",
newValue: resolvedNew ?? "-",
type,
});
}
return diffs;
}

View File

@@ -0,0 +1,165 @@
import dayjs from "dayjs";
type ValueResolver = (
oldVal: any,
newVal: any,
ctx?: Record<string, any>
) => { oldVal: any; newVal: any };
export const diffTranslations: Record<
string,
{ label: string; resolve?: ValueResolver }
> = {
project: {
label: "Projekt",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.projects?.find((i: any) => i.id === o)?.name ?? "-" : "-",
newVal: n ? ctx?.projects?.find((i: any) => i.id === n)?.name ?? "-" : "-",
}),
},
title: { label: "Titel" },
type: { label: "Typ" },
notes: { label: "Notizen" },
link: { label: "Link" },
start: {
label: "Start",
resolve: (o, n) => ({
oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-",
newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-",
}),
},
end: {
label: "Ende",
resolve: (o, n) => ({
oldVal: o ? dayjs(o).format("DD.MM.YYYY HH:mm") : "-",
newVal: n ? dayjs(n).format("DD.MM.YYYY HH:mm") : "-",
}),
},
birthday: {
label: "Geburtstag",
resolve: (o, n) => ({
oldVal: o ? dayjs(o).format("DD.MM.YYYY") : "-",
newVal: n ? dayjs(n).format("DD.MM.YYYY") : "-",
}),
},
resources: {
label: "Resourcen",
resolve: (o, n) => ({
oldVal: Array.isArray(o) ? o.map((i: any) => i.title).join(", ") : "-",
newVal: Array.isArray(n) ? n.map((i: any) => i.title).join(", ") : "-",
}),
},
customerNumber: { label: "Kundennummer" },
active: {
label: "Aktiv",
resolve: (o, n) => ({
oldVal: o === true ? "Aktiv" : "Gesperrt",
newVal: n === true ? "Aktiv" : "Gesperrt",
}),
},
isCompany: {
label: "Firmenkunde",
resolve: (o, n) => ({
oldVal: o === true ? "Firma" : "Privatkunde",
newVal: n === true ? "Firma" : "Privatkunde",
}),
},
special: { label: "Adresszusatz" },
street: { label: "Straße & Hausnummer" },
city: { label: "Ort" },
zip: { label: "Postleitzahl" },
country: { label: "Land" },
web: { label: "Webseite" },
email: { label: "E-Mail" },
tel: { label: "Telefon" },
ustid: { label: "USt-ID" },
role: { label: "Rolle" },
phoneHome: { label: "Festnetz" },
phoneMobile: { label: "Mobiltelefon" },
salutation: { label: "Anrede" },
firstName: { label: "Vorname" },
lastName: { label: "Nachname" },
name: { label: "Name" },
nameAddition: { label: "Name Zusatz" },
approved: { label: "Genehmigt" },
manufacturer: { label: "Hersteller" },
purchasePrice: { label: "Kaufpreis" },
purchaseDate: { label: "Kaufdatum" },
serialNumber: { label: "Seriennummer" },
usePlanning: { label: "In Plantafel verwenden" },
currentSpace: { label: "Lagerplatz" },
customer: {
label: "Kunde",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.customers?.find((i: any) => i.id === o)?.name ?? "-" : "-",
newVal: n ? ctx?.customers?.find((i: any) => i.id === n)?.name ?? "-" : "-",
}),
},
vendor: {
label: "Lieferant",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.vendors?.find((i: any) => i.id === o)?.name ?? "-" : "-",
newVal: n ? ctx?.vendors?.find((i: any) => i.id === n)?.name ?? "-" : "-",
}),
},
description: { label: "Beschreibung" },
categorie: { label: "Kategorie" },
profile: {
label: "Mitarbeiter",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-",
newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-",
}),
},
plant: {
label: "Objekt",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.plants?.find((i: any) => i.id === o)?.name ?? "-" : "-",
newVal: n ? ctx?.plants?.find((i: any) => i.id === n)?.name ?? "-" : "-",
}),
},
annualPaidLeaveDays: { label: "Urlaubstage" },
employeeNumber: { label: "Mitarbeiternummer" },
weeklyWorkingDays: { label: "Wöchentliche Arbeitstage" },
weeklyWorkingHours: { label: "Wöchentliche Arbeitszeit" },
customerRef: { label: "Referenz des Kunden" },
licensePlate: { label: "Kennzeichen" },
tankSize: { label: "Tankvolumen" },
towingCapacity: { label: "Anhängelast" },
color: { label: "Farbe" },
customPaymentDays: { label: "Zahlungsziel in Tagen" },
customSurchargePercentage: { label: "Individueller Aufschlag" },
powerInKW: { label: "Leistung" },
driver: {
label: "Fahrer",
resolve: (o, n, ctx) => ({
oldVal: o ? ctx?.profiles?.find((i: any) => i.id === o)?.fullName ?? "-" : "-",
newVal: n ? ctx?.profiles?.find((i: any) => i.id === n)?.fullName ?? "-" : "-",
}),
},
projecttype: { label: "Projekttyp" },
fixed: {
label: "Festgeschrieben",
resolve: (o, n) => ({
oldVal: o === true ? "Ja" : "Nein",
newVal: n === true ? "Ja" : "Nein",
}),
},
archived: {
label: "Archiviert",
resolve: (o, n) => ({
oldVal: o === true ? "Ja" : "Nein",
newVal: n === true ? "Ja" : "Nein",
}),
},
};

View File

@@ -0,0 +1,45 @@
import axios from "axios"
const AxiosEE = axios.create({
baseURL: process.env.EMAILENGINE_URL ||"https://ee.fedeo.io/v1",
headers: {
Authorization: `Bearer ${process.env.EMAILENGINE_TOKEN || "dcd8209bc5371c728f9ec951600afcfc74e8c391a7e984b2a6df9c4665dc7ad6"}`,
Accept: "application/json",
},
})
export async function sendMailAsUser(
to: string,
subject: string,
html: string,
text: string,
account: string,
cc: string,
bcc: string,
attachments: any,
): Promise<{ success: boolean; info?: any; error?: any }> {
try {
const sendData = {
to: to.split(";").map(i => { return {address: i}}),
cc: cc ? cc.split(";").map((i:any) => { return {address: i}}) : null,
bcc: bcc ? bcc.split(";").map((i:any) => { return {address: i}}) : null,
subject,
text,
html,
attachments
}
if(sendData.cc === null) delete sendData.cc
if(sendData.bcc === null) delete sendData.bcc
const {data} = await AxiosEE.post(`/account/${account}/submit`, sendData)
return { success: true, info: data }
} catch (err) {
console.error("❌ Fehler beim Mailversand:", err)
return { success: false, error: err }
}
}

View File

@@ -0,0 +1,388 @@
import xmlbuilder from "xmlbuilder";
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween.js"
import {BlobWriter, Data64URIReader, TextReader, TextWriter, ZipWriter} from "@zip.js/zip.js";
import {FastifyInstance} from "fastify";
import {GetObjectCommand} from "@aws-sdk/client-s3";
import {s3} from "../s3";
import {secrets} from "../secrets";
dayjs.extend(isBetween)
const getCreatedDocumentTotal = (item) => {
let totalNet = 0
let total19 = 0
let total7 = 0
item.rows.forEach(row => {
if(!['pagebreak','title','text'].includes(row.mode)){
let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3)
totalNet = totalNet + Number(rowPrice)
if(row.taxPercent === 19) {
// @ts-ignore
total19 = total19 + Number(rowPrice * 0.19)
} else if(row.taxPercent === 7) {
// @ts-ignore
total7 = total7 + Number(rowPrice * 0.07)
}
}
})
let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
return {
totalNet: totalNet,
total19: total19,
total7: total7,
totalGross: totalGross,
}
}
const escapeString = (str) => {
str = (str ||"")
.replaceAll("\n","")
.replaceAll(";","")
.replaceAll(/\r/g,"")
.replaceAll(/"/g,"")
.replaceAll(/ü/g,"ue")
.replaceAll(/ä/g,"ae")
.replaceAll(/ö/g,"oe")
return str
}
const displayCurrency = (input, onlyAbs = false) => {
if(onlyAbs) {
return Math.abs(input).toFixed(2).replace(".",",")
} else {
return input.toFixed(2).replace(".",",")
}
}
export async function buildExportZip(server: FastifyInstance, tenant: number, startDate: string, endDate: string, beraternr: string, mandantennr: string): Promise<Buffer> {
try {
const zipFileWriter = new BlobWriter()
const zipWriter = new ZipWriter(zipFileWriter)
//Basic Information
let header = `"EXTF";700;21;"Buchungsstapel";13;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto`
//Get Bookings
const {data:statementallocationsRaw,error: statementallocationsError} = await server.supabase.from("statementallocations").select('*, account(*), bs_id(*, account(*)), cd_id(*,customer(*)), ii_id(*, vendor(*)), vendor(*), customer(*), ownaccount(*)').eq("tenant", tenant);
let {data:createddocumentsRaw,error: createddocumentsError} = await server.supabase.from("createddocuments").select('*,customer(*)').eq("tenant", tenant).in("type",["invoices","advanceInvoices","cancellationInvoices"]).eq("state","Gebucht").eq("archived",false)
let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant).eq("state","Gebucht").eq("archived",false)
const {data:accounts} = await server.supabase.from("accounts").select()
const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single()
let createddocuments = createddocumentsRaw.filter(i => dayjs(i.documentDate).isBetween(startDate,endDate,"day","[]"))
let incominginvoices = incominginvoicesRaw.filter(i => dayjs(i.date).isBetween(startDate,endDate,"day","[]"))
let statementallocations = statementallocationsRaw.filter(i => dayjs(i.bs_id.date).isBetween(startDate,endDate,"day","[]"))
const {data:filesCreateddocuments, error: filesErrorCD} = await server.supabase.from("files").select().eq("tenant",tenant).or(`createddocument.in.(${createddocuments.map(i => i.id).join(",")})`)
const {data:filesIncomingInvoices, error: filesErrorII} = await server.supabase.from("files").select().eq("tenant",tenant).or(`incominginvoice.in.(${incominginvoices.map(i => i.id).join(",")})`)
const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => {
console.log(filePath)
const command = new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: filePath,
})
const { Body, ContentType } = await s3.send(command)
const chunks: any[] = []
// @ts-ignore
for await (const chunk of Body) {
chunks.push(chunk)
}
const buffer = Buffer.concat(chunks)
const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}`
const dataURLReader = new Data64URIReader(dataURL)
await zipWriter.add(`${fileId}.${downloadFilePath.split(".").pop()}`, dataURLReader)
//await fs.writeFile(`./output/${fileId}.${downloadFilePath.split(".").pop()}`, buffer, () => {});
console.log(`File added to Zip`);
};
for (const file of filesCreateddocuments) {
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
}
for (const file of filesIncomingInvoices) {
await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id);
}
let bookingLines = []
createddocuments.forEach(createddocument => {
let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id);
let total = 0
let typeString = ""
if(createddocument.type === "invoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
console.log()
if(createddocument.usedAdvanceInvoices.length > 0){
createddocument.usedAdvanceInvoices.forEach(usedAdvanceInvoice => {
total -= getCreatedDocumentTotal(createddocumentsRaw.find(i => i.id === usedAdvanceInvoice)).totalGross
})
}
console.log(total)
typeString = "AR"
} else if(createddocument.type === "advanceInvoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
typeString = "ARAbschlag"
} else if(createddocument.type === "cancellationInvoices") {
total = getCreatedDocumentTotal(createddocument).totalGross
typeString = "ARStorno"
}
let shSelector = "S"
if(Math.sign(total) === 1) {
shSelector = "S"
} else if (Math.sign(total) === -1) {
shSelector = "H"
}
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${createddocument.customer.customerNumber};8400;"";${dayjs(createddocument.documentDate).format("DDMM")};"${createddocument.documentNumber}";;;"${`${typeString} ${createddocument.documentNumber} - ${createddocument.customer.name}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${createddocument.customer.name}";"Kundennummer";"${createddocument.customer.customerNumber}";"Belegnummer";"${createddocument.documentNumber}";"Leistungsdatum";"${dayjs(createddocument.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(createddocument.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
})
incominginvoices.forEach(incominginvoice => {
console.log(incominginvoice.id);
incominginvoice.accounts.forEach(account => {
let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id);
let accountData = accounts.find(i => i.id === account.account)
let buschluessel: string = "9"
if(account.taxType === '19'){
buschluessel = "9"
} else if(account.taxType === 'null') {
buschluessel = ""
} else if(account.taxType === '7') {
buschluessel = "8"
} else if(account.taxType === '19I') {
buschluessel = "19"
} else if(account.taxType === '7I') {
buschluessel = "18"
} else {
buschluessel = "-"
}
let shSelector = "S"
let amountGross = account.amountGross ? account.amountGross : account.amountNet + account.amountTax
if(Math.sign(amountGross) === 1) {
shSelector = "S"
} else if(Math.sign(amountGross) === -1) {
shSelector = "H"
}
let text = `ER ${incominginvoice.reference}: ${escapeString(incominginvoice.description)}`.substring(0,59)
console.log(incominginvoice)
bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${incominginvoice.vendor.vendorNumber};"${buschluessel}";${dayjs(incominginvoice.date).format("DDMM")};"${incominginvoice.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${incominginvoice.vendor.name}";"Kundennummer";"${incominginvoice.vendor.vendorNumber}";"Belegnummer";"${incominginvoice.reference}";"Leistungsdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
})
})
statementallocations.forEach(statementallocation => {
let shSelector = "S"
if(Math.sign(statementallocation.amount) === 1) {
shSelector = "S"
} else if(Math.sign(statementallocation.amount) === -1) {
shSelector = "H"
}
if(statementallocation.cd_id) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"H";;;;;${statementallocation.cd_id.customer.customerNumber};${statementallocation.bs_id.account.datevNumber};"3";${dayjs(statementallocation.cd_id.documentDate).format("DDMM")};"${statementallocation.cd_id.documentNumber}";;;"${`ZE${statementallocation.description}${escapeString(statementallocation.bs_id.text)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.cd_id.customer.name}";"Kundennummer";"${statementallocation.cd_id.customer.customerNumber}";"Belegnummer";"${statementallocation.cd_id.documentNumber}";"Leistungsdatum";"${dayjs(statementallocation.cd_id.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.cd_id.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.ii_id) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ii_id.vendor.vendorNumber};"";${dayjs(statementallocation.ii_id.date).format("DDMM")};"${statementallocation.ii_id.reference}";;;"${`ZA${statementallocation.description} ${escapeString(statementallocation.bs_id.text)} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ii_id.vendor.name}";"Kundennummer";"${statementallocation.ii_id.vendor.vendorNumber}";"Belegnummer";"${statementallocation.ii_id.reference}";"Leistungsdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.account) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.account.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.account.number} - ${escapeString(statementallocation.account.label)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.bs_id.credName}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.vendor) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.vendor.vendorNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.vendor.vendorNumber} - ${escapeString(statementallocation.vendor.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.vendor.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.customer) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.customer.customerNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.customer.customerNumber} - ${escapeString(statementallocation.customer.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.customer.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
} else if(statementallocation.ownaccount) {
bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ownaccount.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.ownaccount.number} - ${escapeString(statementallocation.ownaccount.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ownaccount.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`)
}
})
let csvString = `${header}\n${colHeaders}\n`;
bookingLines.forEach(line => {
csvString += `${line}\n`;
})
const buchungsstapelReader = new TextReader(csvString)
await zipWriter.add(`EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, buchungsstapelReader)
/*fs.writeFile(`output/EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvString, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
// Kreditoren/Debitoren
let headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
let colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist`
const {data:customers} = await server.supabase.from("customers").select().eq("tenant",tenant).order("customerNumber")
const {data:vendors} = await server.supabase.from("vendors").select().eq("tenant",tenant).order("vendorNumber")
let bookinglinesStammdaten = []
customers.forEach(customer => {
bookinglinesStammdaten.push(`${customer.customerNumber};"${customer.isCompany ? customer.name.substring(0,48): ''}";;"${!customer.isCompany ? (customer.lastname ? customer.lastname : customer.name) : ''}";"${!customer.isCompany ? (customer.firstname ? customer.firstname : '') : ''}";;${customer.isCompany ? 2 : 1};;;;;;;;"STR";"${customer.infoData.street ? customer.infoData.street : ''}";;"${customer.infoData.zip ? customer.infoData.zip : ''}";"${customer.infoData.city ? customer.infoData.city : ''}";;;"${customer.infoData.special ? customer.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
})
vendors.forEach(vendor => {
bookinglinesStammdaten.push(`${vendor.vendorNumber};"${vendor.name.substring(0,48)}";;;;;2;;;;;;;;"STR";"${vendor.infoData.street ? vendor.infoData.street : ''}";;"${vendor.infoData.zip ? vendor.infoData.zip : ''}";"${vendor.infoData.city ? vendor.infoData.city : ''}";;;"${vendor.infoData.special ? vendor.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`)
})
let csvStringStammdaten = `${headerStammdaten}\n${colHeadersStammdaten}\n`;
bookinglinesStammdaten.forEach(line => {
csvStringStammdaten += `${line}\n`;
})
const stammdatenReader = new TextReader(csvStringStammdaten)
await zipWriter.add(`EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, stammdatenReader)
/*fs.writeFile(`output/EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringStammdaten, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
//Sachkonten
let headerSachkonten = `"EXTF";700;20;"Kontenbeschriftungen";3;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Sachkonten";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`
let colHeadersSachkonten = `Konto;Kontenbeschriftung;Sprach-ID;Kontenbeschriftung lang`
const {data:bankaccounts} = await server.supabase.from("bankaccounts").select().eq("tenant",tenant).order("datevNumber")
let bookinglinesSachkonten = []
bankaccounts.forEach(bankaccount => {
bookinglinesSachkonten.push(`${bankaccount.datevNumber};"${bankaccount.name}";"de-DE";`)
})
let csvStringSachkonten = `${headerSachkonten}\n${colHeadersSachkonten}\n`;
bookinglinesSachkonten.forEach(line => {
csvStringSachkonten += `${line}\n`;
})
const sachkontenReader = new TextReader(csvStringSachkonten)
await zipWriter.add(`EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, sachkontenReader)
/*fs.writeFile(`output/EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringSachkonten, 'utf8', function (err) {
if (err) {
console.log('Some error occured - file either not saved or corrupted file saved.');
console.log(err);
} else{
console.log('It\'s saved!');
}
});*/
let obj = {
archive: {
'@version':"5.0",
"@generatingSystem":"fedeo.de",
"@xsi:schemaLocation":"http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd",
"@xmlns":"http://xml.datev.de/bedi/tps/document/v05.0",
"@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance",
header: {
date: dayjs().format("YYYY-MM-DDTHH:mm:ss")
},
content: {
document: []
}
}
}
filesCreateddocuments.forEach(file => {
obj.archive.content.document.push({
"@guid": file.id,
extension: {
"@xsi:type":"File",
"@name":`${file.id}.pdf`
}
})
})
filesIncomingInvoices.forEach(file => {
obj.archive.content.document.push({
"@guid": file.id,
extension: {
"@xsi:type":"File",
"@name":`${file.id}.pdf`
}
})
})
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
//console.log(doc.end({pretty: true}));
const documentsReader = new TextReader(doc.end({pretty: true}))
await zipWriter.add(`document.xml`, documentsReader)
/*function toBuffer(arrayBuffer) {
const buffer = Buffer.alloc(arrayBuffer.byteLength);
const view = new Uint8Array(arrayBuffer);
for (let i = 0; i < buffer.length; ++i) {
buffer[i] = view[i];
}
return buffer;
}*/
const arrayBuffer = await (await zipWriter.close()).arrayBuffer()
return Buffer.from(arrayBuffer)
} catch(error) {
console.log(error)
}
}

View File

@@ -0,0 +1,114 @@
import xmlbuilder from "xmlbuilder";
import {randomUUID} from "node:crypto";
import dayjs from "dayjs";
export const createSEPAExport = async (server,idsToExport, tenant_id) => {
const {data,error} = await server.supabase.from("createddocuments").select().eq("tenant", tenant_id).in("id", idsToExport)
const {data:tenantData,error:tenantError} = await server.supabase.from("tenants").select().eq("id", tenant_id).single()
console.log(tenantData)
console.log(tenantError)
console.log(data)
let transactions = []
let obj = {
Document: {
'@xmlns':"urn:iso:std:iso:20022:tech:xsd:pain.008.001.02",
'CstmrDrctDbtInitn': {
'GrpHdr': {
'MsgId': randomUUID(),
'CredDtTm': dayjs().format("YYYY-MM-DDTHH:mm:ss"),
'NbOfTxs': transactions.length,
'CtrlSum': 0, // TODO: Total Sum
'InitgPty': {
'Nm': tenantData.name
}
},
'PmtInf': {
'PmtInfId': "", // TODO: Mandatsreferenz,
'PmtMtd': "DD",
'BtchBookg': "true", // TODO: BatchBooking,
'NbOfTxs': transactions.length,
'CtrlSum': 0, //TODO: Total Sum
'PmtTpInf': {
'SvcLvl': {
'Cd': "SEPA"
},
'LclInstrm': {
'Cd': "CORE" // Core für BASIS / B2B für Firmen
},
'SeqTp': "RCUR" // TODO: Unterscheidung Einmalig / Wiederkehrend
},
'ReqdColltnDt': dayjs().add(3, "days").format("YYYY-MM-DDTHH:mm:ss"),
'Cdtr': {
'Nm': tenantData.name
},
'CdtrAcct': {
'Id': {
'IBAN': "DE" // TODO: IBAN Gläubiger EINSETZEN
}
},
'CdtrAgt': {
'FinInstnId': {
'BIC': "BIC" // TODO: BIC des Gläubigers einsetzen
}
},
'ChrgBr': "SLEV",
'CdtrSchmeId': {
'Id': {
'PrvtId': {
'Othr': {
'Id': tenantData.creditorId,
'SchmeNm': {
'Prty': "SEPA"
}
}
}
}
},
//TODO ITERATE ALL INVOICES HERE
'DrctDbtTxInf': {
'PmtId': {
'EndToEndId': "" // TODO: EINDEUTIGE ReFERENZ wahrscheinlich RE Nummer
},
'InstdAmt': {
'@Ccy':"EUR",
'#text':100 //TODO: Rechnungssumme zwei NK mit Punkt
},
'DrctDbtTx': {
'MndtRltdInf': {
'MndtId': "", // TODO: Mandatsref,
'DtOfSgntr': "", //TODO: Unterschrieben am,
'AmdmntInd': "" //TODO: Mandat geändert
}
},
'DbtrAgt': {
'FinInstnId': {
'BIC': "", //TODO: BIC Debtor
}
},
'Dbtr': {
'Nm': "" // TODO NAME Debtor
},
'DbtrAcct': {
'Id': {
'IBAN': "DE" // TODO IBAN Debtor
}
},
'RmtInf': {
'Ustrd': "" //TODO Verwendungszweck Rechnungsnummer
}
}
}
}
}
}
let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true})
console.log(doc.end({pretty:true}))
}

View File

@@ -0,0 +1,95 @@
import { PutObjectCommand } from "@aws-sdk/client-s3"
import { s3 } from "./s3"
import { secrets } from "./secrets"
// Drizzle schema
import { files } from "../../db/schema"
import { eq } from "drizzle-orm"
import { FastifyInstance } from "fastify"
export const saveFile = async (
server: FastifyInstance,
tenant: number,
messageId: string | number | null, // Typ angepasst (oft null bei manueller Gen)
attachment: any, // Kann File, Buffer oder Mailparser-Objekt sein
folder: string | null,
type: string | null,
other: Record<string, any> = {}
) => {
try {
// ---------------------------------------------------
// 1⃣ FILE ENTRY ANLEGEN
// ---------------------------------------------------
const insertRes = await server.db
.insert(files)
.values({
tenant,
folder,
type,
...other
})
.returning()
const created = insertRes?.[0]
if (!created) {
console.error("File creation failed (no row returned)")
return null
}
// Name ermitteln (Fallback Logik)
// Wenn attachment ein Buffer ist, muss der Name in 'other' stehen oder generiert werden
const filename = attachment.filename || other.filename || `${created.id}.pdf`
// ---------------------------------------------------
// 2⃣ BODY & CONTENT TYPE ERMITTELN
// ---------------------------------------------------
let body: Buffer | Uint8Array | string
let contentType = type || "application/octet-stream"
if (Buffer.isBuffer(attachment)) {
// FALL 1: RAW BUFFER (von finishManualGeneration)
body = attachment
// ContentType wurde oben schon über 'type' Parameter gesetzt (z.B. application/pdf)
} else if (typeof File !== "undefined" && attachment instanceof File) {
// FALL 2: BROWSER FILE
body = Buffer.from(await attachment.arrayBuffer())
contentType = attachment.type || contentType
} else if (attachment.content) {
// FALL 3: MAILPARSER OBJECT
body = attachment.content
contentType = attachment.contentType || contentType
} else {
console.error("saveFile: Unknown attachment format")
return null
}
// ---------------------------------------------------
// 3⃣ S3 UPLOAD
// ---------------------------------------------------
const key = `${tenant}/filesbyid/${created.id}/${filename}`
await s3.send(
new PutObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: key,
Body: body,
ContentType: contentType,
ContentLength: body.length // <--- WICHTIG: Behebt den AWS Fehler
})
)
// ---------------------------------------------------
// 4⃣ PATH IN DB SETZEN
// ---------------------------------------------------
await server.db
.update(files)
.set({ path: key })
.where(eq(files.id, created.id))
console.log(`File saved: ${key}`)
return { id: created.id, key }
} catch (err) {
console.error("saveFile error:", err)
return null
}
}

View File

@@ -0,0 +1,174 @@
import {FastifyInstance} from "fastify";
// import { PNG } from 'pngjs'
// import { ready as zplReady } from 'zpl-renderer-js'
// import { Utils } from '@mmote/niimbluelib'
// import { createCanvas } from 'canvas'
// import bwipjs from 'bwip-js'
// import Sharp from 'sharp'
// import fs from 'fs'
import { tenants } from "../../db/schema"
import { eq } from "drizzle-orm"
export const useNextNumberRangeNumber = async (
server: FastifyInstance,
tenantId: number,
numberRange: string
) => {
// 1⃣ Tenant laden
const [tenant] = await server.db
.select()
.from(tenants)
.where(eq(tenants.id, tenantId))
if (!tenant) {
throw new Error(`Tenant ${tenantId} not found`)
}
const numberRanges = tenant.numberRanges || {}
if (!numberRanges[numberRange]) {
throw new Error(`Number range '${numberRange}' not found`)
}
const current = numberRanges[numberRange]
// 2⃣ Used Number generieren
const usedNumber =
(current.prefix || "") +
current.nextNumber +
(current.suffix || "")
// 3⃣ nextNumber erhöhen
const updatedRanges = {
// @ts-ignore
...numberRanges,
[numberRange]: {
...current,
nextNumber: current.nextNumber + 1
}
}
// 4⃣ Tenant aktualisieren
await server.db
.update(tenants)
.set({ numberRanges: updatedRanges })
.where(eq(tenants.id, tenantId))
return { usedNumber }
}
/*
export async function encodeBase64ToNiimbot(base64Png, printDirection = 'top') {
// 1⃣ PNG dekodieren
const buffer = Buffer.from(base64Png, 'base64')
const png = PNG.sync.read(buffer) // liefert {width, height, data: Uint8Array(RGBA)}
const { width, height, data } = png
console.log(width, height, data)
const cols = printDirection === 'left' ? height : width
const rows = printDirection === 'left' ? width : height
const rowsData = []
console.log(cols)
if (cols % 8 !== 0) throw new Error('Column count must be multiple of 8')
// 2⃣ Zeilenweise durchgehen und Bits bilden
for (let row = 0; row < rows; row++) {
let isVoid = true
let blackPixelsCount = 0
const rowData = new Uint8Array(cols / 8)
for (let colOct = 0; colOct < cols / 8; colOct++) {
let pixelsOctet = 0
for (let colBit = 0; colBit < 8; colBit++) {
const x = printDirection === 'left' ? row : colOct * 8 + colBit
const y = printDirection === 'left' ? height - 1 - (colOct * 8 + colBit) : row
const idx = (y * width + x) * 4
const lum = 0.299 * data[idx] + 0.587 * data[idx + 1] + 0.114 * data[idx + 2]
const isBlack = lum < 128
if (isBlack) {
pixelsOctet |= 1 << (7 - colBit)
isVoid = false
blackPixelsCount++
}
}
rowData[colOct] = pixelsOctet
}
const newPart = {
dataType: isVoid ? 'void' : 'pixels',
rowNumber: row,
repeat: 1,
rowData: isVoid ? undefined : rowData,
blackPixelsCount,
}
if (rowsData.length === 0) {
rowsData.push(newPart)
} else {
const last = rowsData[rowsData.length - 1]
let same = newPart.dataType === last.dataType
if (same && newPart.dataType === 'pixels') {
same = Utils.u8ArraysEqual(newPart.rowData, last.rowData)
}
if (same) last.repeat++
else rowsData.push(newPart)
if (row % 200 === 199) {
rowsData.push({
dataType: 'check',
rowNumber: row,
repeat: 0,
rowData: undefined,
blackPixelsCount: 0,
})
}
}
}
return { cols, rows, rowsData }
}
export async function generateLabel(context,width,height) {
// Canvas für Hintergrund & Text
const canvas = createCanvas(width, height)
const ctx = canvas.getContext('2d')
// Hintergrund weiß
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, width, height)
// Überschrift
ctx.fillStyle = '#000000'
ctx.font = '32px Arial'
ctx.fillText(context.text, 20, 40)
// 3) DataMatrix
const dataMatrixPng = await bwipjs.toBuffer({
bcid: 'datamatrix',
text: context.datamatrix,
scale: 6,
})
// Basisbild aus Canvas
const base = await Sharp(canvas.toBuffer())
.png()
.toBuffer()
// Alles zusammen compositen
const final = await Sharp(base)
.composite([
{ input: dataMatrixPng, top: 60, left: 20 },
])
.png()
.toBuffer()
fs.writeFileSync('label.png', final)
// Optional: Base64 zurückgeben (z.B. für API)
const base64 = final.toString('base64')
return base64
}*/

204
backend/src/utils/gpt.ts Normal file
View File

@@ -0,0 +1,204 @@
import dayjs from "dayjs";
import axios from "axios";
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { Blob } from "buffer";
import { FastifyInstance } from "fastify";
import { s3 } from "./s3";
import { secrets } from "./secrets";
// Drizzle schema
import { vendors, accounts } from "../../db/schema";
import {eq} from "drizzle-orm";
let openai: OpenAI | null = null;
// ---------------------------------------------------------
// INITIALIZE OPENAI
// ---------------------------------------------------------
export const initOpenAi = async () => {
openai = new OpenAI({
apiKey: secrets.OPENAI_API_KEY,
});
};
// ---------------------------------------------------------
// STREAM → BUFFER
// ---------------------------------------------------------
async function streamToBuffer(stream: any): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on("data", (chunk: Buffer) => chunks.push(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(Buffer.concat(chunks)));
});
}
// ---------------------------------------------------------
// GPT RESPONSE FORMAT (Zod Schema)
// ---------------------------------------------------------
const InstructionFormat = z.object({
invoice_number: z.string(),
invoice_date: z.string(),
invoice_duedate: z.string(),
invoice_type: z.string(),
delivery_type: z.string(),
delivery_note_number: z.string(),
reference: z.string(),
issuer: z.object({
id: z.number().nullable().optional(),
name: z.string(),
address: z.string(),
phone: z.string(),
email: z.string(),
bank: z.string(),
bic: z.string(),
iban: z.string(),
}),
recipient: z.object({
name: z.string(),
address: z.string(),
phone: z.string(),
email: z.string(),
}),
invoice_items: z.array(
z.object({
description: z.string(),
unit: z.string(),
quantity: z.number(),
total: z.number(),
total_without_tax: z.number(),
tax_rate: z.number(),
ean: z.number().nullable().optional(),
article_number: z.number().nullable().optional(),
account_number: z.number().nullable().optional(),
account_id: z.number().nullable().optional(),
})
),
subtotal: z.number(),
tax_rate: z.number(),
tax: z.number(),
total: z.number(),
terms: z.string(),
});
// ---------------------------------------------------------
// MAIN FUNCTION REPLACES SUPABASE VERSION
// ---------------------------------------------------------
export const getInvoiceDataFromGPT = async function (
server: FastifyInstance,
file: any,
tenantId: number
) {
await initOpenAi();
if (!openai) {
throw new Error("OpenAI not initialized. Call initOpenAi() first.");
}
console.log(`📄 Reading invoice file ${file.id}`);
// ---------------------------------------------------------
// 1) DOWNLOAD PDF FROM S3
// ---------------------------------------------------------
let fileData: Buffer;
try {
const command = new GetObjectCommand({
Bucket: secrets.S3_BUCKET,
Key: file.path,
});
const response: any = await s3.send(command);
fileData = await streamToBuffer(response.Body);
} catch (err) {
console.log(`❌ S3 Download failed for file ${file.id}`, err);
return null;
}
// Only process PDFs
if (!file.path.toLowerCase().endsWith(".pdf")) {
server.log.warn(`Skipping non-PDF file ${file.id}`);
return null;
}
const fileBlob = new Blob([fileData], { type: "application/pdf" });
// ---------------------------------------------------------
// 2) SEND FILE TO PDF → TEXT API
// ---------------------------------------------------------
const form = new FormData();
form.append("fileInput", fileBlob, file.path.split("/").pop());
form.append("outputFormat", "txt");
let extractedText: string;
try {
const res = await axios.post(
"http://23.88.52.85:8080/api/v1/convert/pdf/text",
form,
{
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${secrets.STIRLING_API_KEY}`,
},
}
);
extractedText = res.data;
} catch (err) {
console.log("❌ PDF OCR API failed", err);
return null;
}
// ---------------------------------------------------------
// 3) LOAD VENDORS + ACCOUNTS (DRIZZLE)
// ---------------------------------------------------------
const vendorList = await server.db
.select({ id: vendors.id, name: vendors.name })
.from(vendors)
.where(eq(vendors.tenant,tenantId));
const accountList = await server.db
.select({
id: accounts.id,
label: accounts.label,
number: accounts.number,
})
.from(accounts);
// ---------------------------------------------------------
// 4) GPT ANALYSIS
// ---------------------------------------------------------
const completion = await openai.chat.completions.parse({
model: "gpt-4o",
store: true,
response_format: zodResponseFormat(InstructionFormat as any, "instruction"),
messages: [
{ role: "user", content: extractedText },
{
role: "user",
content:
"You extract structured invoice data.\n\n" +
`VENDORS: ${JSON.stringify(vendorList)}\n` +
`ACCOUNTS: ${JSON.stringify(accountList)}\n\n` +
"Match issuer by name to vendor.id.\n" +
"Match invoice items to account id based on label/number.\n" +
"Convert dates to YYYY-MM-DD.\n" +
"Keep invoice items in original order.\n",
},
],
});
const parsed = completion.choices[0].message.parsed;
console.log(`🧾 Extracted invoice data for file ${file.id}`);
return parsed;
};

View File

@@ -0,0 +1,106 @@
// 🔧 Hilfsfunktionen
import { FastifyInstance } from "fastify"
import { eq, ilike, and } from "drizzle-orm"
import { contacts, customers } from "../../db/schema"
// -------------------------------------------------------------
// Extract Domain
// -------------------------------------------------------------
export function extractDomain(email: string) {
if (!email) return null
const parts = email.split("@")
return parts.length === 2 ? parts[1].toLowerCase() : null
}
// -------------------------------------------------------------
// Kunde oder Kontakt anhand E-Mail oder Domain finden
// -------------------------------------------------------------
export async function findCustomerOrContactByEmailOrDomain(
server: FastifyInstance,
fromMail: string,
tenantId: number
) {
const sender = fromMail.toLowerCase()
const senderDomain = extractDomain(sender)
if (!senderDomain) return null
// 1⃣ Direkter Match über Contacts (email)
const contactMatch = await server.db
.select({
id: contacts.id,
customer: contacts.customer,
})
.from(contacts)
.where(
and(
eq(contacts.email, sender),
eq(contacts.tenant, tenantId)
)
)
.limit(1)
if (contactMatch.length && contactMatch[0].customer) {
return {
customer: contactMatch[0].customer,
contact: contactMatch[0].id,
}
}
// 2⃣ Kunden anhand Domain vergleichen
const allCustomers = await server.db
.select({
id: customers.id,
infoData: customers.infoData,
})
.from(customers)
.where(eq(customers.tenant, tenantId))
for (const c of allCustomers) {
const info = c.infoData || {}
// @ts-ignore
const email = info.email?.toLowerCase()
//@ts-ignore
const invoiceEmail = info.invoiceEmail?.toLowerCase()
const emailDomain = extractDomain(email)
const invoiceDomain = extractDomain(invoiceEmail)
if (
sender === email ||
sender === invoiceEmail ||
senderDomain === emailDomain ||
senderDomain === invoiceDomain
) {
return { customer: c.id, contact: null }
}
}
return null
}
// -------------------------------------------------------------
// getNestedValue (für Sortierung & Suche im Backend)
// -------------------------------------------------------------
export function getNestedValue(obj: any, path: string): any {
return path
.split(".")
.reduce((acc, part) => (acc && acc[part] !== undefined ? acc[part] : undefined), obj)
}
// -------------------------------------------------------------
// compareValues (Sortierung für paginated)
// -------------------------------------------------------------
export function compareValues(a: any, b: any): number {
if (a === b) return 0
if (a == null) return 1
if (b == null) return -1
// String Compare
if (typeof a === "string" && typeof b === "string") {
return a.localeCompare(b)
}
// Numerisch
return a < b ? -1 : 1
}

View File

@@ -0,0 +1,70 @@
import { FastifyInstance } from "fastify"
export async function insertHistoryItem(
server: FastifyInstance,
params: {
tenant_id: number
created_by: string | null
entity: string
entityId: string | number
action: "created" | "updated" | "unchanged" | "deleted" | "archived"
oldVal?: any
newVal?: any
text?: string
}
) {
const textMap = {
created: `Neuer Eintrag in ${params.entity} erstellt`,
updated: `Eintrag in ${params.entity} geändert`,
archived: `Eintrag in ${params.entity} archiviert`,
deleted: `Eintrag in ${params.entity} gelöscht`
}
const columnMap: Record<string, string> = {
customers: "customer",
vendors: "vendor",
projects: "project",
plants: "plant",
contacts: "contact",
inventoryitems: "inventoryitem",
products: "product",
profiles: "profile",
absencerequests: "absencerequest",
events: "event",
tasks: "task",
vehicles: "vehicle",
costcentres: "costcentre",
ownaccounts: "ownaccount",
documentboxes: "documentbox",
hourrates: "hourrate",
services: "service",
roles: "role",
checks: "check",
spaces: "space",
trackingtrips: "trackingtrip",
createddocuments: "createddocument",
inventoryitemgroups: "inventoryitemgroup",
bankstatements: "bankstatement"
}
const fkColumn = columnMap[params.entity]
if (!fkColumn) {
server.log.warn(`Keine History-Spalte für Entity: ${params.entity}`)
return
}
const entry = {
tenant: params.tenant_id,
created_by: params.created_by,
text: params.text || textMap[params.action],
action: params.action,
[fkColumn]: params.entityId,
oldVal: params.oldVal ? JSON.stringify(params.oldVal) : null,
newVal: params.newVal ? JSON.stringify(params.newVal) : null
}
const { error } = await server.supabase.from("historyitems").insert([entry])
if (error) { // @ts-ignore
console.log(error)
}
}

View File

@@ -0,0 +1,37 @@
import nodemailer from "nodemailer"
import {secrets} from "./secrets"
export let transporter = null
export const initMailer = async () => {
transporter = nodemailer.createTransport({
host: secrets.MAILER_SMTP_HOST,
port: Number(secrets.MAILER_SMTP_PORT) || 587,
secure: secrets.MAILER_SMTP_SSL === "true", // true für 465, false für andere Ports
auth: {
user: secrets.MAILER_SMTP_USER,
pass: secrets.MAILER_SMTP_PASS,
},
})
console.log("Mailer Initialized!")
}
export async function sendMail(
to: string,
subject: string,
html: string
): Promise<{ success: boolean; info?: any; error?: any }> {
try {
const info = await transporter.sendMail({
from: secrets.MAILER_FROM,
to,
subject,
html,
})
// Nodemailer liefert eine Info-Response zurück
return { success: true, info }
} catch (err) {
console.error("❌ Fehler beim Mailversand:", err)
return { success: false, error: err }
}
}

View File

@@ -0,0 +1,15 @@
import bcrypt from "bcrypt"
export function generateRandomPassword(length = 12): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"
let password = ""
for (let i = 0; i < length; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length))
}
return password
}
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 10
return bcrypt.hash(password, saltRounds)
}

1126
backend/src/utils/pdf.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
import {
accounts,
bankaccounts,
bankrequisitions,
bankstatements,
contacts,
contracts,
costcentres,
createddocuments,
customers,
files,
filetags,
folders,
hourrates,
incominginvoices,
inventoryitemgroups,
inventoryitems,
letterheads,
ownaccounts,
plants,
productcategories,
products,
projects,
projecttypes,
serialExecutions,
servicecategories,
services,
spaces,
statementallocations,
tasks,
texttemplates,
units,
vehicles,
vendors
} from "../../db/schema";
export const resourceConfig = {
projects: {
searchColumns: ["name"],
mtoLoad: ["customer","plant","contract","projecttype"],
mtmLoad: ["tasks", "files","createddocuments"],
table: projects,
numberRangeHolder: "projectNumber"
},
customers: {
searchColumns: ["name", "customerNumber", "firstname", "lastname", "notes"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts"],
table: customers,
numberRangeHolder: "customerNumber",
},
contacts: {
searchColumns: ["firstName", "lastName", "email", "phone", "notes"],
table: contacts,
mtoLoad: ["customer","vendor"]
},
contracts: {
table: contracts,
searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"],
numberRangeHolder: "contractNumber",
},
plants: {
table: plants,
mtoLoad: ["customer"],
mtmLoad: ["projects","tasks","files"],
},
projecttypes: {
table: projecttypes
},
vendors: {
table: vendors,
searchColumns: ["name","vendorNumber","notes","defaultPaymentType"],
numberRangeHolder: "vendorNumber",
},
files: {
table: files
},
folders: {
table: folders
},
filetags: {
table: filetags
},
inventoryitems: {
table: inventoryitems,
numberRangeHolder: "articleNumber",
},
inventoryitemgroups: {
table: inventoryitemgroups
},
products: {
table: products,
searchColumns: ["name","manufacturer","ean","barcode","description","manfacturer_number","article_number"],
},
productcategories: {
table: productcategories
},
services: {
table: services,
mtoLoad: ["unit"],
searchColumns: ["name","description"],
},
servicecategories: {
table: servicecategories
},
units: {
table: units,
},
vehicles: {
table: vehicles,
searchColumns: ["name","license_plate","vin","color"],
},
hourrates: {
table: hourrates,
searchColumns: ["name"],
},
spaces: {
table: spaces,
searchColumns: ["name","space_number","type","info_data"],
numberRangeHolder: "spaceNumber",
},
ownaccounts: {
table: ownaccounts,
searchColumns: ["name","description","number"],
},
costcentres: {
table: costcentres,
searchColumns: ["name","number","description"],
mtoLoad: ["vehicle","project","inventoryitem"],
numberRangeHolder: "number",
},
tasks: {
table: tasks,
},
letterheads: {
table: letterheads,
},
createddocuments: {
table: createddocuments,
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"],
mtmLoad: ["statementallocations","files","createddocuments"],
mtmListLoad: ["statementallocations"],
},
texttemplates: {
table: texttemplates
},
incominginvoices: {
table: incominginvoices,
mtmLoad: ["statementallocations","files"],
mtmListLoad: ["statementallocations"],
mtoLoad: ["vendor"],
},
statementallocations: {
table: statementallocations,
mtoLoad: ["customer","vendor","incominginvoice","createddocument","ownaccount","bankstatement"]
},
accounts: {
table: accounts,
},
bankstatements: {
table: bankstatements,
mtmListLoad: ["statementallocations"],
mtmLoad: ["statementallocations"],
},
bankaccounts: {
table: bankaccounts,
},
bankrequisitions: {
table: bankrequisitions,
},
serialexecutions: {
table: serialExecutions
}
}

18
backend/src/utils/s3.ts Normal file
View File

@@ -0,0 +1,18 @@
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"
import {secrets} from "./secrets";
export let s3 = null
export const initS3 = async () => {
s3 = new S3Client({
endpoint: secrets.S3_ENDPOINT, // z. B. http://localhost:9000 für MinIO
region: secrets.S3_REGION,
credentials: {
accessKeyId: secrets.S3_ACCESS_KEY,
secretAccessKey: secrets.S3_SECRET_KEY,
},
forcePathStyle: true, // wichtig für MinIO
})
}

View File

@@ -0,0 +1,63 @@
import {InfisicalSDK} from "@infisical/sdk"
const client = new InfisicalSDK({
siteUrl: "https://secrets.fedeo.io"
})
export let secrets = {
} as {
COOKIE_SECRET: string
JWT_SECRET: string
PORT: number
HOST: string
DATABASE_URL: string
SUPABASE_URL: string
SUPABASE_SERVICE_ROLE_KEY: string
S3_BUCKET: string
ENCRYPTION_KEY: string
MAILER_SMTP_HOST: string
MAILER_SMTP_PORT: number
MAILER_SMTP_SSL: string
MAILER_SMTP_USER: string
MAILER_SMTP_PASS: string
MAILER_FROM: string
S3_ENDPOINT: string
S3_REGION: string
S3_ACCESS_KEY: string
S3_SECRET_KEY: string
M2M_API_KEY: string
API_BASE_URL: string
GOCARDLESS_BASE_URL: string
GOCARDLESS_SECRET_ID: string
GOCARDLESS_SECRET_KEY: string
DOKUBOX_IMAP_HOST: string
DOKUBOX_IMAP_PORT: number
DOKUBOX_IMAP_SECURE: boolean
DOKUBOX_IMAP_USER: string
DOKUBOX_IMAP_PASSWORD: string
OPENAI_API_KEY: string
STIRLING_API_KEY: string
}
export async function loadSecrets () {
await client.auth().universalAuth.login({
clientId: process.env.INFISICAL_CLIENT_ID,
clientSecret: process.env.INFISICAL_CLIENT_SECRET,
});
const allSecrets = await client.secrets().listSecrets({
environment: "dev", // stg, dev, prod, or custom environment slugs
projectId: "39774094-2aaf-49fb-a213-d6b2c10f6144"
});
allSecrets.secrets.forEach(secret => {
secrets[secret.secretKey] = secret.secretValue
})
console.log("✅ Secrets aus Infisical geladen");
console.log(Object.keys(secrets).length + " Stück")
}

40
backend/src/utils/sort.ts Normal file
View File

@@ -0,0 +1,40 @@
/**
* Sortiert ein Array von Objekten anhand einer Spalte.
*
* @param data Array von Objekten
* @param column Sortierspalte (Property-Name im Objekt)
* @param ascending true = aufsteigend, false = absteigend
*/
export function sortData<T extends Record<string, any>>(
data: T[],
column?: keyof T | null,
ascending: boolean = true
): T[] {
if (!column) return data
return [...data].sort((a, b) => {
const valA = a[column]
const valB = b[column]
// null/undefined nach hinten
if (valA == null && valB != null) return 1
if (valB == null && valA != null) return -1
if (valA == null && valB == null) return 0
// Zahlenvergleich
if (typeof valA === "number" && typeof valB === "number") {
return ascending ? valA - valB : valB - valA
}
// Datumsvergleich
// @ts-ignore
if (valA instanceof Date && valB instanceof Date) {
return ascending ? valA.getTime() - valB.getTime() : valB.getTime() - valA.getTime()
}
// Fallback: Stringvergleich
return ascending
? String(valA).localeCompare(String(valB))
: String(valB).localeCompare(String(valA))
})
}

View File

@@ -0,0 +1,51 @@
export const renderAsCurrency = (value: string | number,currencyString = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}`
}
export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => {
if(removeLinebreaks) {
input = input.replaceAll("\n","")
}
let splitStrings: string[] = []
input.split("\n").forEach(string => {
splitStrings.push(string)
})
let returnSplitStrings: string[] = []
splitStrings.forEach(string => {
let regex = / /gi, result, indices = [];
while ( (result = regex.exec(string)) ) {
indices.push(result.index);
}
let lastIndex = 0
if(string.length > maxSplitLength) {
let tempStrings = []
for (let i = maxSplitLength; i < string.length; i = i + maxSplitLength) {
let nearestIndex = indices.length > 0 ? indices.reduce(function(prev, curr) {
return (Math.abs(curr - i) < Math.abs(prev - i) ? curr : prev);
}) : i
tempStrings.push(string.substring(lastIndex,nearestIndex))
lastIndex = indices.length > 0 ? nearestIndex + 1 : nearestIndex
}
tempStrings.push(string.substring(lastIndex,input.length))
returnSplitStrings.push(...tempStrings)
} else {
returnSplitStrings.push(string)
}
})
return returnSplitStrings
}