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; }