initial commit
This commit is contained in:
123
convex/lib/categorize.ts
Normal file
123
convex/lib/categorize.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
const EXPENSE_RULES: Array<{ category: string; keywords: string[] }> = [
|
||||
{ category: "Haustier (Tierarzt & -bedarf)", keywords: ["FRESSNAPF", "KLEINTIERPRAXIS", "FUTTERKRIPPE", "TIERARZT", "TIERHEIM"] },
|
||||
{ category: "Gesundheit & Apotheke", keywords: ["APOTHEKE", "ZAHNARZT", "MVZ", "LABOR DR", "KLINIK", "PRAXIS DR"] },
|
||||
{ category: "Miete & Wohnen", keywords: ["MIETE", "BETRIEBSKOSTEN", "KAUTIONSKASSE", "KAUTION"] },
|
||||
{ category: "Wasser & Abwasser", keywords: ["WASSERWERKE", "VERBANDSGEMEINDEKASSE", "ABWASSER"] },
|
||||
{ category: "Rundfunkbeitrag", keywords: ["RUNDFUNK"] },
|
||||
{ category: "Kredite & Finanzierung", keywords: ["ING-DIBA", "RATENKREDIT", "KASHRES", "KASH RES"] },
|
||||
{ category: "Versicherung & Vorsorge", keywords: ["VERSICHER", "WUERTT", "KRANKENVERS", "RIESTER", "FONDS-RENTE", "DAK", "HAFTPFLICHT"] },
|
||||
{ category: "KFZ-Steuer", keywords: ["KFZ-STEUER", "KFZ STEUER"] },
|
||||
{ category: "Energie (Strom/Heizung)", keywords: ["EPRIMO", "STADTWERKE", "MONTANA", "GASVERSORG"] },
|
||||
{ category: "Telekom, Internet & Hosting", keywords: ["O2", "TELEFONICA", "VODAFONE", "TELEKOM", "1&1", "1UND1", "CONGSTAR", "NETCUP", "CLOUDFLARE", "GOOGLE CLOUD"] },
|
||||
{ category: "Abos & Streaming", keywords: ["YOUTUBE", "NETFLIX", "SPOTIFY", "DISNEY", "AMAZON PRIME", "GOOGLE *", "APPLE.COM", "ITUNES", "C2 CIRCLE"] },
|
||||
{ category: "Beiträge & Mitgliedschaften", keywords: ["OFFIZIERGESELLSCHAFT", "CASINO JULIUS", "MITGLIEDSBEITR", "ADAC", "TCG ", "E.V", "E. V", "SCHUFA"] },
|
||||
{ category: "Soziales, Kita & Bildung", keywords: ["AWO KREISVERBAND", "STADTBIBLIOTHEK", "BILDUNGSTICKET"] },
|
||||
{ category: "Tanken", keywords: ["KAUFL SERVICE STATION", "SHELL", "ESSO", "ARAL", "JET ", "STAR ", "STAR T", "TANKSTELLE", "AVIA", "TOTAL", "HEM "] },
|
||||
{ category: "Auto & Werkstatt", keywords: ["AUTO TELEMANN", "AUTOHAUS", "WERKSTATT", "REIFEN"] },
|
||||
{ category: "Lebensmittel & Supermarkt", keywords: ["KAUFLAND", "LIDL", "ALDI", "NETTO", "PENNY", "REWE", "EDEKA", "NORMA", "KAUFPARK", "GETRAENKE", "GETRÄNKE", "E-CENTER", "E CENTER", "SPAR-LAND", "DISKA", "GLOBUS", "TRANSGOURMET", "PHILIPPS", "SPAR LAND"] },
|
||||
{ category: "Drogerie & Körperpflege", keywords: ["ROSSMANN", "DM-DROGERIE", "DM DROGERIE", "DM FIL", "MUELLER", "BUDNI", "HAARSCHARF", "FRISEUR"] },
|
||||
{ category: "Baumarkt, Garten & Möbel", keywords: ["OBI", "SONDERPREIS BAUMARKT", "BAUZENTRUM", "DEHNER", "BAUMARKT", "HORNBACH", "IKEA", "XXXLUTZ", "WOKO", "SUEDWEST"] },
|
||||
{ category: "Haushalt & Discounter", keywords: ["ACTION", "TEDI", "WOOLWORTH", "NKD"] },
|
||||
{ category: "Kleidung & Schuhe", keywords: ["TAKKO", "RENO", "DEICHMANN", "KIK", "C&A", "H&M", "ZALANDO", "FASHION"] },
|
||||
{ category: "Restaurant, Lieferdienst & Gastro", keywords: ["MCDONALD", "BURGER", "LIEFERANDO", "DOMINO", "KFC", "SUBWAY", "BÄCKER", "BAECKER", "RESTAURANT", "CAFE", "IMBISS", "VIELFALTMENU", "VIELFALTMENUE", "SYSTEMGASTRONOMIE", "BRAUHAUS", "SUN HOUSE", "SANIFAIR", "PANUSSIS", "LEITERMANN", "NYX*", "VENDING"] },
|
||||
{ category: "Parken", keywords: ["EASYPARK", "PARKHAUS", "APCOA", "PARK "] },
|
||||
{ category: "Bahn, ÖPNV & Mobilität", keywords: ["DB FERNVERKEHR", "DB VERTRIEB", "BAHN", "FLIXBUS", "FLIX", "NAHVERKEHR", "STAEDTISCHE VERKEHRS", "MILES MOBILITY", "SVZ "] },
|
||||
{ category: "Freizeit & Kultur", keywords: ["FILMPALAST", "KINO", "SCHWIMM", "THEATER"] },
|
||||
{ category: "Shopping & Online", keywords: ["AMAZON", "PAYPAL", "ETSY", "EBAY", "TEMU", "OTTO "] },
|
||||
{ category: "Porto & Versand", keywords: ["DEUTSCHE POST", "DHL"] },
|
||||
];
|
||||
|
||||
const INCOME_RULES: Array<{ category: string; keywords: string[] }> = [
|
||||
{ category: "Trennungsgeld / Mietzuschuss", keywords: ["TG-MIETE", "VZ TG"] },
|
||||
{ category: "Gehalt & Besoldung", keywords: ["BESOLDUNG", "LOHN", "GEHALT"] },
|
||||
{ category: "Kindergeld", keywords: ["FAMILIENKASSE", "KINDERGELD"] },
|
||||
{ category: "Eigene Überträge / Einzahlungen", keywords: ["ÜBERTRAG", "UEBERTRAG"] },
|
||||
{ category: "Zinsen", keywords: ["ZINSEN", "ABSCHLUSS"] },
|
||||
];
|
||||
|
||||
function matchesKeywords(text: string, keywords: string[]): boolean {
|
||||
return keywords.some((kw) => text.includes(kw));
|
||||
}
|
||||
|
||||
function matchesOwnNameTransfer(text: string, ownNames: string[]): boolean {
|
||||
for (const name of ownNames) {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) continue;
|
||||
const pattern = new RegExp(`EMPFÄNGER:\\s*${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
|
||||
if (pattern.test(text)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function categorize(
|
||||
rawText: string,
|
||||
amount: number,
|
||||
vorgang: string,
|
||||
ownNames: string[],
|
||||
): string {
|
||||
const text = rawText.toUpperCase();
|
||||
const vorgangUpper = (vorgang ?? "").toUpperCase();
|
||||
|
||||
if (amount < 0) {
|
||||
if (matchesOwnNameTransfer(text, ownNames)) {
|
||||
return "Interne & private Überträge";
|
||||
}
|
||||
for (const rule of EXPENSE_RULES) {
|
||||
if (matchesKeywords(text, rule.keywords)) {
|
||||
return rule.category;
|
||||
}
|
||||
}
|
||||
if (vorgangUpper === "AUSZAHLUNG GAA" || vorgangUpper === "AUSZAHLUNG") {
|
||||
return "Bargeldauszahlung";
|
||||
}
|
||||
if (
|
||||
vorgangUpper === "KONTOFÜHRUNGSENTGELT" ||
|
||||
["ENTGELT", "GEBÜHR", "GEBUEHR", "ABSCHLUSS ZINSEN"].some((k) => vorgangUpper.includes(k))
|
||||
) {
|
||||
return "Bankgebühren & Zinsen";
|
||||
}
|
||||
return "Sonstiges";
|
||||
}
|
||||
|
||||
for (const rule of INCOME_RULES) {
|
||||
if (matchesKeywords(text, rule.keywords)) {
|
||||
return rule.category;
|
||||
}
|
||||
}
|
||||
return "Sonstige Einnahmen";
|
||||
}
|
||||
|
||||
export function parseCounterpartyFromBuchungstext(buchungstext: string): {
|
||||
counterparty?: string;
|
||||
description: string;
|
||||
rawText: string;
|
||||
} {
|
||||
const rawText = buchungstext.trim();
|
||||
const normalized = rawText.replace(/\s+/g, " ");
|
||||
|
||||
const auftraggeber = normalized.match(/Auftraggeber:\s*([^]+?)(?=\s*(?:Empfänger:|Buchungstext:|$))/i);
|
||||
if (auftraggeber?.[1]) {
|
||||
return { counterparty: auftraggeber[1].trim(), description: auftraggeber[1].trim(), rawText };
|
||||
}
|
||||
|
||||
const empfaenger = normalized.match(/Empfänger:\s*([^]+?)(?=\s*(?:Buchungstext:|$))/i);
|
||||
if (empfaenger?.[1]) {
|
||||
return { counterparty: empfaenger[1].trim(), description: empfaenger[1].trim(), rawText };
|
||||
}
|
||||
|
||||
const buchungstextMatch = normalized.match(/Buchungstext:\s*([^]+)$/i);
|
||||
if (buchungstextMatch?.[1]) {
|
||||
return { description: buchungstextMatch[1].trim(), rawText };
|
||||
}
|
||||
|
||||
return { description: normalized, rawText };
|
||||
}
|
||||
|
||||
export function parseGermanAmount(value: string): number {
|
||||
const cleaned = value.trim().replace(/\./g, "").replace(",", ".");
|
||||
return Math.round(parseFloat(cleaned) * 100) / 100;
|
||||
}
|
||||
|
||||
export function roundEur(amount: number): number {
|
||||
return Math.round(amount * 100) / 100;
|
||||
}
|
||||
Reference in New Issue
Block a user