Alle drei Bank-Parser gegen die realen Fixtures gehaertet: - DKB/VR/HVB: Sammlung endet am Schlusssaldo (break), damit Summen, Rechnungsabschluss und Hinweisseiten nicht mehr in die letzte Buchung rutschen. - Generische, rein strukturelle Noise-Filter fuer wiederkehrende Seitenfuss-/Kopfbloecke und Blattwechsel-Reste (keine Kontodaten hart codiert); Kopf-/Hinweisbloecke werden als Sequenz uebersprungen. - VR: Rekonstruktion harter Zeilenumbrueche im Verwendungszweck unter konservativen Schutzbedingungen (nur purpose, nicht counterparty). - Neue Noise- und expected-Value-Tests; expected_*.json gitignored. - Audit-Werkzeug scripts/parser_audit.py ergaenzt (nur lokale Nutzung). Transaktionszahlen und Saldo-Differenzen unveraendert; max. Laenge des Verwendungszwecks je Bank deutlich unter 300 Zeichen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
import json
|
|
import re
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
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()
|