"""Erkennung von Zahlungsdaten aus echten Benachrichtigungs-Mails. Die Muster unter ``fixtures/mail`` sind echte Mails von Raiffeisen und Revolut, anonymisiert. Aufbau, Zeilenumbrüche und Formulierungen sind unverändert – nur daran lässt sich prüfen, ob die Erkennung wirklich trägt. """ import email import email.policy import pathlib from datetime import date from app.services import mail_parse MUSTER = pathlib.Path(__file__).parent / "fixtures" / "mail" def _lies(name: str): with open(MUSTER / name, "rb") as f: msg = email.message_from_binary_file(f, policy=email.policy.default) teil = msg.get_body(preferencelist=("plain",)) if teil is not None: text = teil.get_content() else: text = mail_parse.html_zu_text(msg.get_body(preferencelist=("html",)).get_content()) return msg["Subject"], text, msg def test_raiffeisen_zahlungsstatus(): """Beschriftete Felder in einer Klartext-Mail: ``Betrag: 14.90 CHF``.""" betreff, text, msg = _lies("raiffeisen_zahlungsstatus.eml") e = mail_parse.parse(betreff, text, email.utils.parsedate_to_datetime(msg["Date"])) assert e.betrag.wert == 1490 assert e.waehrung.wert == "CHF" assert e.datum.wert == date(2026, 7, 31) assert e.empfaenger.wert == "Sunrise GmbH / Yallo" assert e.richtung.wert == "expense" assert e.unklar == [] # Jeder Wert muss erklären können, woher er stammt. assert "Betrag" in e.betrag.quelle assert "Ausführungsdatum" in e.datum.quelle def test_revolut_ueberweisung(): """Nur HTML, Wert steht UNTER der Beschriftung, Datum ohne Jahr.""" betreff, text, msg = _lies("revolut_ueberweisung.eml") e = mail_parse.parse(betreff, text, email.utils.parsedate_to_datetime(msg["Date"])) assert e.betrag.wert == 32400 assert e.waehrung.wert == "EUR" assert e.empfaenger.wert == "Alex Muster" assert e.richtung.wert == "expense" # „gesendet“ # „Samstag, 1 August, 12:41“ trägt kein Jahr – der Zeitstempel der Mail # ist hier die verlässlichere Quelle. assert e.datum.wert == date(2026, 8, 1) assert "E-Mail" in e.datum.quelle def test_stilbloecke_liefern_keine_betraege(): """CSS enthält Zahlen wie ``width:100%`` und ``480px`` – kein Geld.""" roh = "
Betrag: 12,50 EUR
" text = mail_parse.html_zu_text(roh) assert "480" not in text e = mail_parse.parse("", text) assert e.betrag.wert == 1250 def test_nicht_verbuchter_auftrag_gilt_als_unklar(): """Ein erfasster, aber nicht verbuchter Auftrag ist noch nicht geflossen.""" text = ("Auf Ihrem Privatkonto hat der Status eines Zahlungsauftrages geändert.\n" " Begünstigter: Muster AG\n" " Betrag: 40.00 CHF\n" " Ausführungsdatum: 05.03.2026\n" " Neuer Status: Zur Visierung\n") e = mail_parse.parse("Änderung Zahlungsstatus", text) assert "status" in e.unklar e2 = mail_parse.parse("Änderung Zahlungsstatus", text.replace("Zur Visierung", "Verbucht")) assert e2.unklar == [] def test_mehrere_betraege_ohne_beschriftung_bleiben_unklar(): """Lieber nachfragen als still den falschen Betrag buchen.""" e = mail_parse.parse("Kontoinformation", "Umsatz 25,00 EUR\nNeuer Saldo 813,55 EUR\n") assert "betrag" in e.unklar assert e.betrag is None def test_beschriftung_schlaegt_den_saldo(): """Steht der Betrag beschriftet da, gewinnt er gegen den Kontostand.""" e = mail_parse.parse("Kontoinformation", "Betrag: 25,00 EUR\nNeuer Saldo: 813,55 EUR\n") assert e.betrag.wert == 2500 def test_gutschrift_wird_als_einnahme_erkannt(): e = mail_parse.parse("Du hast 50 € erhalten", "Empfangen\n50 €\n") assert e.richtung.wert == "income" assert e.betrag.wert == 5000 def test_blosse_zahlen_sind_keine_betraege(): e = mail_parse.parse("Auftrag 12345", "Referenz 998877\nBestellnummer 4711\n") assert e.betrag is None assert "betrag" in e.unklar