121 lines
4.3 KiB
Python
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 == ""
|