diff --git a/finance/app/services/importer.py b/finance/app/services/importer.py index daee923..8e8d96d 100644 --- a/finance/app/services/importer.py +++ b/finance/app/services/importer.py @@ -23,6 +23,15 @@ def _find_or_create_account(session: Session, bank: str, iban: str | None, filen return acc +def _error_statement(session: Session, filename: str, message: str) -> Statement: + stmt = Statement(filename=filename, bank="unbekannt", account_id=None, + status="error", error_message=message) + session.add(stmt) + session.commit() + session.refresh(stmt) + return stmt + + def process_pdf(session: Session, path: Path) -> Statement: settings = get_settings() filename = path.name @@ -30,12 +39,15 @@ def process_pdf(session: Session, path: Path) -> Statement: try: parsed = parse_pdf(path) except ParserError as exc: - stmt = Statement(filename=filename, bank="unbekannt", account_id=None, - status="error", error_message=str(exc)) - session.add(stmt) - session.commit() - session.refresh(stmt) - return stmt + return _error_statement(session, filename, str(exc)) + except Exception as exc: + # Fremdbibliotheken (pdfplumber/pdfminer) werfen bei strukturell + # kaputten PDFs eigene Exception-Typen statt ParserError (z.B. + # PdfminerException "No /Root object!"). Ohne diesen Fang wuerde ein + # kaputtes Upload zu einem unbehandelten 500 fuehren statt zu einem + # sauberen Fehler-Import wie bei "Bank nicht erkannt". Die Datei + # bleibt dabei unangetastet im Posteingang. + return _error_statement(session, filename, f"PDF nicht lesbar: {exc}") account = _find_or_create_account(session, parsed.bank, parsed.iban, filename) diff --git a/finance/app/templates/planning.html b/finance/app/templates/planning.html index e7a8870..848c968 100644 --- a/finance/app/templates/planning.html +++ b/finance/app/templates/planning.html @@ -204,7 +204,7 @@ {% for l in loans %} {% else %} @@ -354,9 +354,18 @@ recSelect.disabled = isCategory; } - function toggleScenarioLoan(scenarioId, loanId, checked) { + function toggleScenarioLoan(scenarioId, loanId, checked, checkbox) { fetch('/api/scenarios/' + scenarioId + '/loans/' + loanId, { method: checked ? 'POST' : 'DELETE' }) - .then(function () { window.location.reload(); }); + .then(function (resp) { + if (!resp.ok) { + throw new Error('HTTP ' + resp.status); + } + window.location.reload(); + }) + .catch(function (err) { + checkbox.checked = !checked; + alert('Zuordnung fehlgeschlagen: ' + err.message); + }); } var loadedSchedules = {}; diff --git a/finance/tests/test_import_api.py b/finance/tests/test_import_api.py index 21caba4..76cc69d 100644 --- a/finance/tests/test_import_api.py +++ b/finance/tests/test_import_api.py @@ -71,3 +71,24 @@ def test_upload_rejects_non_pdf(client, tmp_path, monkeypatch): r = client.post("/api/imports/upload", headers=H, files={"file": ("x.txt", b"not a pdf", "text/plain")}) assert r.status_code == 400 + + +def test_upload_corrupt_pdf_produces_error_statement_not_500(client, tmp_path, monkeypatch): + # Datei besteht die Endungspruefung (.pdf), ist aber strukturell kein + # gueltiges PDF -> pdfplumber wirft eine eigene Exception (keine + # ParserError). Das darf nicht als unbehandelter 500 durchschlagen, + # sondern muss wie "Bank nicht erkannt" als sauberer Fehler-Import + # sichtbar werden. + monkeypatch.setenv("FB_INBOX_DIR", str(tmp_path / "inbox")) + monkeypatch.setenv("FB_UPLOADS_DIR", str(tmp_path / "uploads")) + from app.config import get_settings + get_settings.cache_clear() + r = client.post("/api/imports/upload", headers=H, + files={"file": ("kaputt.pdf", b"not a pdf", "application/pdf")}) + assert r.status_code == 201 + body = r.json() + assert body["status"] == "error" + assert "PDF nicht lesbar" in body["error_message"] + # Datei bleibt im Posteingang liegen, wird nicht ins Uploads-Verzeichnis verschoben. + assert (tmp_path / "inbox" / "kaputt.pdf").exists() + assert not (tmp_path / "uploads" / "kaputt.pdf").exists()