diff --git a/backend/app/services/bank_sync.py b/backend/app/services/bank_sync.py index 4ff133a..41f91ed 100644 --- a/backend/app/services/bank_sync.py +++ b/backend/app/services/bank_sync.py @@ -188,10 +188,15 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict except ValueError: verbindung.valid_until = None - neu = 0 - for konto in daten.get("accounts", []): + uebermittelt = daten.get("accounts") or [] + neu = bekannt = ohne_kennung = 0 + for konto in uebermittelt: uid = konto.get("uid") if not uid: + # Nicht stillschweigend verwerfen: Sonst sieht „0 Konten" genauso + # aus, als hätte die Bank gar nichts geschickt – und man sucht den + # Fehler bei der Bank statt hier. + ohne_kennung += 1 continue vorhanden = ( db.query(BankAccountLink) @@ -199,7 +204,9 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict .first() ) if vorhanden is not None: + # Schon bekannt – die Zuordnung bleibt bewusst erhalten. vorhanden.connection_id = verbindung.id + bekannt += 1 continue db.add(BankAccountLink( owner_id=user.id, @@ -212,8 +219,15 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict neu += 1 db.commit() - return {"connection_id": verbindung.id, "konten": neu, - "valid_until": verbindung.valid_until.isoformat() if verbindung.valid_until else None} + return { + "connection_id": verbindung.id, + "konten": neu + bekannt, # nutzbar, egal ob neu oder schon bekannt + "neu": neu, + "bekannt": bekannt, + "uebermittelt": len(uebermittelt), + "ohne_kennung": ohne_kennung, + "valid_until": verbindung.valid_until.isoformat() if verbindung.valid_until else None, + } def _betrag_cents(umsatz: dict) -> int | None: diff --git a/backend/tests/test_bank.py b/backend/tests/test_bank.py index 4b95a1b..c2e430d 100644 --- a/backend/tests/test_bank.py +++ b/backend/tests/test_bank.py @@ -480,3 +480,42 @@ def test_genauer_name_schlaegt_teiltreffer(admin, monkeypatch): monkeypatch.setattr(bank_api.BankClient, "_anfrage", _anfrage) namen = [b["name"] for b in admin.get("/api/bank/banks?search=revolut").json()["treffer"]] assert namen == ["Revolut", "Revolut Bank UAB", "Banca Revoluta"] + + +def test_bereits_bekanntes_konto_zaehlt_nicht_als_null(admin, fake_bank): + """„0 Konten" darf nicht heissen, dass alles schon bekannt war. + + Beim erneuten Verbinden derselben Bank blieb die Zaehlung auf null stehen, + obwohl die Konten da waren – das liest sich wie ein Fehlschlag. + """ + _app(admin) + konten = [{"uid": f"acc-{secrets.token_hex(4)}", "name": "Girokonto", + "currency": "EUR", "account_id": {"iban": "DE02120300000000202051"}}] + fake_bank(konten=konten) + + def verbinde(): + state = admin.post("/api/bank/connect", + json={"aspsp_name": "N26", "country": "DE"}).json()["state"] + return admin.post(f"/api/bank/callback?state={state}&code=guter-code").json() + + erst = verbinde() + assert erst["neu"] == 1 and erst["konten"] == 1 + + zweit = verbinde() + assert zweit["neu"] == 0 + assert zweit["bekannt"] == 1 + assert zweit["konten"] == 1, "Bekannte Konten sind trotzdem nutzbar" + + +def test_konten_ohne_kennung_werden_gemeldet_statt_verworfen(admin, fake_bank): + """Ohne diese Zahl sieht „nichts übermittelt" genauso aus wie „alles + verworfen" – und man sucht den Fehler bei der falschen Seite.""" + _app(admin) + fake_bank(konten=[{"name": "Ohne Kennung", "currency": "EUR"}]) + state = admin.post("/api/bank/connect", + json={"aspsp_name": "N26", "country": "DE"}).json()["state"] + antwort = admin.post(f"/api/bank/callback?state={state}&code=guter-code").json() + + assert antwort["uebermittelt"] == 1 + assert antwort["ohne_kennung"] == 1 + assert antwort["konten"] == 0 diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 60694cd..19c301c 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -587,7 +587,7 @@ "connect": "Verbinden", "notMapped": "noch keinem Konto zugeordnet", "mapHint": "Ohne Zuordnung wird von diesem Konto nichts abgerufen.", - "noAccountsFound": "Es wurden keine Konten übermittelt.", + "noAccountsFound": "Es wurden keine Konten übermittelt. Im eingeschränkten Betrieb gibt die Bank nur heraus, was du bei Enable Banking unter „Activate by linking accounts“ verknüpft hast – dort diese Bank ergänzen und hier neu verbinden.", "sync": "Umsätze holen", "synced": "{{geholt}} Umsätze geprüft · {{neu}} neu im Eingang.", "noMapping": "Kein Konto zugeordnet – ordne oben zuerst zu, dann wird abgerufen.", @@ -620,6 +620,9 @@ "failed": "Das hat nicht geklappt", "failedHint": "Der Code von der Bank gilt nur einmal und nur kurz. Starte die Verbindung in den Einstellungen neu.", "missing": "Die Bank hat keinen Code zurückgegeben.", - "toSettings": "Zu den Einstellungen" + "toSettings": "Zu den Einstellungen", + "noAccounts": "Verbunden, aber die Bank hat kein Konto übermittelt", + "noAccountsHint": "Im eingeschränkten Betrieb gibt die Bank nur Konten heraus, die du bei Enable Banking selbst verknüpft hast. Öffne dort deine Anwendung, drück „Activate by linking accounts“ und wähle diese Bank – danach hier noch einmal verbinden.", + "noAccountsDropped": "Die Bank hat {{n}} Einträge geschickt, aber ohne Kennung – damit lässt sich nichts abrufen. Das ist ein Fall für den Support von Enable Banking; schick mir die Meldung, dann formuliere ich die Anfrage." } } diff --git a/web/src/locales/en.json b/web/src/locales/en.json index f9c7656..ff69f2c 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -587,7 +587,7 @@ "connect": "Connect", "notMapped": "not mapped to an account yet", "mapHint": "Without a mapping nothing is fetched from this account.", - "noAccountsFound": "No accounts were returned.", + "noAccountsFound": "No accounts were returned. In restricted mode the bank only releases what you linked at Enable Banking under “Activate by linking accounts” – add this bank there and reconnect here.", "sync": "Fetch transactions", "synced": "{{geholt}} transactions checked · {{neu}} new in the inbox.", "noMapping": "No account mapped – map one above first, then it will fetch.", @@ -620,6 +620,9 @@ "failed": "That did not work", "failedHint": "The code from the bank is single-use and short-lived. Start the connection again in the settings.", "missing": "The bank did not return a code.", - "toSettings": "Go to settings" + "toSettings": "Go to settings", + "noAccounts": "Connected, but the bank returned no account", + "noAccountsHint": "In restricted mode the bank only releases accounts you linked at Enable Banking yourself. Open your application there, press “Activate by linking accounts” and pick this bank – then connect again here.", + "noAccountsDropped": "The bank sent {{n}} entries but without an identifier, so nothing can be fetched. That is one for Enable Banking support; send me the message and I will draft the question." } } diff --git a/web/src/pages/BankRueckkehr.jsx b/web/src/pages/BankRueckkehr.jsx index ea180f6..baff473 100644 --- a/web/src/pages/BankRueckkehr.jsx +++ b/web/src/pages/BankRueckkehr.jsx @@ -34,7 +34,7 @@ export default function BankRueckkehr() { return; } api.bankCallback(state, code) - .then((r) => setStand({ laeuft: false, konten: r.konten })) + .then((r) => setStand({ laeuft: false, ...r })) .catch((e) => setStand({ laeuft: false, fehler: e.message })); // Nur einmal ausführen: Der Code lässt sich kein zweites Mal einlösen. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -56,13 +56,25 @@ export default function BankRueckkehr() { )} - {!stand.laeuft && !stand.fehler && ( + {!stand.laeuft && !stand.fehler && stand.konten > 0 && (
{t("bankReturn.next")}
+ {stand.ohne_kennung > 0 + ? t("bankReturn.noAccountsDropped", { n: stand.ohne_kennung }) + : t("bankReturn.noAccountsHint")} +
+