Files
bin/finance/tests/test_bank_parsers.py
wlfb f27eda88f4 fix: Parser-Noise-Filter, Sammelstopp am Schlusssaldo, Audit-Werkzeug
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>
2026-07-19 14:46:32 +02:00

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()