Files
bin/finance/tests/test_bank_parsers.py

121 lines
4.3 KiB
Python

import json
import re
from decimal import Decimal
from pathlib import Path
import pytest
from app.parsers.dkb import _split_counterparty
from app.parsers.registry import parse_pdf
from app.parsers.validate import balance_difference
FIXTURES = Path(__file__).parent / "fixtures"
CASES = [("vr_beispiel.pdf", "vr"), ("hvb_beispiel.pdf", "hvb"),
("dkb_beispiel.pdf", "dkb")]
NOISE = re.compile(
r"(Kontostand|Übertrag|alter Saldo|neuer Saldo|Seite \d|Blatt \d)", re.I)
@pytest.mark.parametrize("filename,bank", CASES)
def test_parse_fixture(filename, bank):
path = FIXTURES / filename
if not path.exists():
pytest.skip(f"Fixture {filename} fehlt")
stmt = parse_pdf(path)
assert stmt.bank == bank
assert stmt.opening_balance is not None
assert stmt.closing_balance is not None
assert len(stmt.transactions) > 0
assert balance_difference(stmt) == Decimal("0")
@pytest.mark.parametrize("filename,bank", CASES)
def test_no_noise_in_parsed_fields(filename, bank):
path = FIXTURES / filename
if not path.exists():
pytest.skip(f"Fixture {filename} fehlt")
stmt = parse_pdf(path)
for t in stmt.transactions:
assert not NOISE.search(t.purpose), f"Noise im Verwendungszweck: #{stmt.transactions.index(t)}"
assert not NOISE.search(t.counterparty), f"Noise im Empfänger: #{stmt.transactions.index(t)}"
assert stmt.period_start <= t.booking_date <= stmt.period_end
@pytest.mark.parametrize("filename,bank", CASES)
def test_expected_values(filename, bank):
path = FIXTURES / filename
exp_path = FIXTURES / f"expected_{bank}.json"
if not path.exists() or not exp_path.exists():
pytest.skip("Fixture oder expected-Datei fehlt")
stmt = parse_pdf(path)
exp = json.loads(exp_path.read_text())
assert len(stmt.transactions) == exp["transaction_count"]
for c in exp["spot_checks"]:
t = stmt.transactions[c["index"]]
assert str(t.booking_date) == c["booking_date"]
assert str(t.amount) == c["amount"]
assert c["counterparty_contains"].lower() in t.counterparty.lower()
assert c["purpose_contains"].lower() in t.purpose.lower()
# --- DKB Empfänger/Zweck-Split-Heuristik (D-T5-1/D-T5-2) -------------------
# Ausschließlich synthetische Zeilen (fiktive Namen/Nummern), keine echten
# Kontodaten.
def test_split_counterparty_legal_form_single():
cp, rest = _split_counterparty(
"Musterfirma Beispiel GmbH Rechnung Nr. F000123 vom 01.01.20")
assert cp == "Musterfirma Beispiel GmbH"
assert rest == "Rechnung Nr. F000123 vom 01.01.20"
def test_split_counterparty_legal_form_et_cie_chain():
cp, rest = _split_counterparty(
"Fiktiv Global S.a.r.l. et Cie S.C.A Onlinedienst Referenz 999")
assert cp == "Fiktiv Global S.a.r.l. et Cie S.C.A"
assert rest == "Onlinedienst Referenz 999"
def test_split_counterparty_reference_boundary_digit_token():
cp, rest = _split_counterparty(
"Beispiel Handelsgesellschaft Auftrag 84210 Sonderzahlung")
assert cp == "Beispiel Handelsgesellschaft Auftrag"
assert rest == "84210 Sonderzahlung"
def test_split_counterparty_reference_boundary_keyword_token():
cp, rest = _split_counterparty(
"Servicepartner Nord Vertrag ABC123 Laufzeit")
assert cp == "Servicepartner Nord"
assert rest == "Vertrag ABC123 Laufzeit"
def test_split_counterparty_no_split_pure_words():
cp, rest = _split_counterparty("MUSTER PERSON Nebenkosten")
assert cp == "MUSTER PERSON Nebenkosten"
assert rest == ""
def test_split_counterparty_siehe_anlage():
cp, rest = _split_counterparty("siehe Anlage Nr. 7")
assert cp == ""
assert rest == "siehe Anlage Nr. 7"
def test_split_counterparty_never_empty_guard():
# Erstes Token enthält eine Ziffer, darf aber nie selbst die
# Referenz-Grenze bilden (Regel greift frühestens ab Token 1) - da kein
# weiteres Token eine Ziffer/Schlüsselwort liefert, bleibt die ganze
# Zeile als counterparty erhalten (nie leer).
cp, rest = _split_counterparty("42 Testfirma Ausgleich")
assert cp == "42 Testfirma Ausgleich"
assert rest == ""
assert cp != ""
# Einzelnes Token (auch mit Ziffer): kein Schnitt möglich, bleibt
# counterparty (nie leer, nie None).
cp2, rest2 = _split_counterparty("12345")
assert cp2 == "12345"
assert rest2 == ""