import { describe, expect, it } from "vitest"; import { auszugLesen, camtLesen, mt940Lesen } from "@/lib/kasse/bankimport"; /** * Kontoauszüge einlesen. * * Die Beispiele sind bewusst so unaufgeräumt wie echte Dateien: mit * Namensraum-Präfix, mit dem Verwendungszweck über mehrere Felder verteilt, * mit Fortsetzungszeilen. Ein Parser, der nur an der eigenen sauberen * Beispieldatei geprüft wurde, geht beim ersten echten Auszug kaputt. * * Wichtig ist außerdem, was der Parser NICHT tut: er bucht nichts. Er liefert * Vorschläge, und alles, was er nicht lesen konnte, steht als Fehler daneben — * statt still zu verschwinden. */ const CAMT = ` DE02701500000000594937 412.90 DBIT
2026-03-12
2026-03-13
2026031200123456 Tankstelle Musterhausen DE12500105170648489890 Sammelrechnung Februar Diesel LF 20
6500.00 CRDT
2026-01-20
Gemeinde Musterhausen Zuschuss Feuerwehr 2026
`; const MT940 = [ ":20:STARTUMS", ":25:DE02701500000000594937", ":28C:00012/001", ":60F:C260301EUR4800,00", ":61:2603120312DR412,90NMSCNONREF", ":86:117?00UEBERWEISUNG?20Sammelrechnung Februar?21Diesel LF 20", "?32Tankstelle Musterhausen", ":61:2601200120CR6500,00NTRFNONREF", ":86:166?00GUTSCHRIFT?20Zuschuss Feuerwehr 2026?32Gemeinde Musterhausen", ":62F:C260331EUR10887,10", ].join("\r\n"); describe("CAMT.053", () => { it("liest Konto, Richtung, Betrag und Datum", () => { const auszug = camtLesen(CAMT); expect(auszug.format).toBe("CAMT.053"); expect(auszug.konto).toBe("DE02701500000000594937"); expect(auszug.fehler).toHaveLength(0); expect(auszug.posten).toHaveLength(2); const [ausgabe, einnahme] = auszug.posten; expect(ausgabe?.art).toBe("AUSGABE"); expect(ausgabe?.betrag.toFixed(2)).toBe("412.90"); expect(ausgabe?.datum.toISOString().slice(0, 10)).toBe("2026-03-12"); expect(einnahme?.art).toBe("EINNAHME"); expect(einnahme?.betrag.toFixed(2)).toBe("6500.00"); }); it("setzt einen über mehrere Ustrd verteilten Verwendungszweck zusammen", () => { const auszug = camtLesen(CAMT); expect(auszug.posten[0]?.verwendungszweck).toBe("Sammelrechnung Februar Diesel LF 20"); }); it("holt die Gegenseite je nach Richtung aus Creditor oder Debtor", () => { const auszug = camtLesen(CAMT); expect(auszug.posten[0]?.gegenseite).toBe("Tankstelle Musterhausen"); expect(auszug.posten[0]?.gegenIban).toBe("DE12500105170648489890"); expect(auszug.posten[1]?.gegenseite).toBe("Gemeinde Musterhausen"); }); it("übernimmt die Kennung der Bank — daran hängt die Doppelerkennung", () => { const auszug = camtLesen(CAMT); expect(auszug.posten[0]?.kennung).toBe("2026031200123456"); }); it("liefert bei zweimaligem Einlesen dieselben Kennungen", () => { // Der eigentliche Zweck: derselbe Auszug zweimal eingelesen darf nicht // zwei Monatssätze in der Kasse ergeben. const erste = camtLesen(CAMT).posten.map((posten) => posten.kennung); const zweite = camtLesen(CAMT).posten.map((posten) => posten.kennung); expect(erste).toEqual(zweite); }); it("kommt mit Namensraum-Präfixen zurecht", () => { const mitPraefix = CAMT.replace(/<(\/?)(Ntry|Amt|CdtDbtInd|Ustrd)\b/g, "<$1ns2:$2"); const auszug = camtLesen(mitPraefix); expect(auszug.posten).toHaveLength(2); }); it("meldet einen unbrauchbaren Umsatz, statt ihn stillschweigend zu übergehen", () => { const kaputt = CAMT.replace("DBIT", ""); const auszug = camtLesen(kaputt); expect(auszug.posten).toHaveLength(1); expect(auszug.fehler).toHaveLength(1); expect(auszug.fehler[0]?.stelle).toBe("Umsatz 1"); }); it("sagt es deutlich, wenn gar kein Umsatz drinsteht", () => { const auszug = camtLesen(""); expect(auszug.posten).toHaveLength(0); expect(auszug.fehler[0]?.grund).toMatch(/Ntry/); }); }); describe("MT940", () => { it("liest Konto, Richtung, Betrag und Buchungsdatum", () => { const auszug = mt940Lesen(MT940); expect(auszug.format).toBe("MT940"); expect(auszug.konto).toBe("DE02701500000000594937"); expect(auszug.fehler).toHaveLength(0); expect(auszug.posten).toHaveLength(2); const [ausgabe, einnahme] = auszug.posten; expect(ausgabe?.art).toBe("AUSGABE"); expect(ausgabe?.betrag.toFixed(2)).toBe("412.90"); // Buchungsdatum 0312 vor Valuta 260312 — beides März 2026. expect(ausgabe?.datum.toISOString().slice(0, 10)).toBe("2026-03-12"); expect(einnahme?.art).toBe("EINNAHME"); expect(einnahme?.betrag.toFixed(2)).toBe("6500.00"); }); it("setzt den Verwendungszweck aus den Unterfeldern ?20 ff. zusammen", () => { const auszug = mt940Lesen(MT940); expect(auszug.posten[0]?.verwendungszweck).toBe("Sammelrechnung Februar Diesel LF 20"); }); it("hängt Fortsetzungszeilen an, statt sie zu verlieren", () => { // ?32 steht im Beispiel auf einer eigenen Zeile ohne Kennsatz. const auszug = mt940Lesen(MT940); expect(auszug.posten[0]?.gegenseite).toBe("Tankstelle Musterhausen"); }); it("liest den Jahreswechsel im Auszug richtig", () => { // Valuta 260105, Buchung 1229: das Buchungsdatum stammt aus dem Vorjahr. const auszug = mt940Lesen( [":25:DE02701500000000594937", ":61:2601051229DR36,00NMSCNONREF", ":86:117?20Kontofuehrung"].join( "\n", ), ); expect(auszug.posten[0]?.datum.toISOString().slice(0, 10)).toBe("2025-12-29"); }); it("kehrt bei Rückbuchungen die Richtung um", () => { const auszug = mt940Lesen( [":25:DE02701500000000594937", ":61:2603120312RD120,00NMSCNONREF", ":86:117?20Ruecklastschrift"].join( "\n", ), ); // RD ist die Rückgabe einer Lastschrift — das Geld kommt zurück. expect(auszug.posten[0]?.art).toBe("EINNAHME"); }); it("meldet eine unlesbare Umsatzzeile mit ihrer Nummer", () => { const auszug = mt940Lesen( [":25:DE02701500000000594937", ":61:Unsinn", ":86:117?20egal"].join("\n"), ); expect(auszug.posten).toHaveLength(0); expect(auszug.fehler[0]?.stelle).toBe("Umsatz 1"); }); it("kommt ohne :86: aus und vergibt trotzdem eine Kennung", () => { const auszug = mt940Lesen( [":25:DE02701500000000594937", ":61:2603120312DR412,90NMSCNONREF"].join("\n"), ); expect(auszug.posten).toHaveLength(1); expect(auszug.posten[0]?.verwendungszweck).toBe("(kein Verwendungszweck)"); expect(auszug.posten[0]?.kennung).not.toBe(""); }); }); describe("Formaterkennung", () => { it("erkennt CAMT am Inhalt, nicht an der Endung", () => { expect(auszugLesen(CAMT).format).toBe("CAMT.053"); }); it("erkennt MT940 an den Kennsätzen", () => { expect(auszugLesen(MT940).format).toBe("MT940"); }); it("sagt bei einem unbekannten Format, was erwartet wird", () => { const auszug = auszugLesen("Datum;Betrag;Zweck\n12.03.2026;-412,90;Diesel"); expect(auszug.posten).toHaveLength(0); expect(auszug.fehler[0]?.grund).toMatch(/CAMT\.053|MT940/); }); });