fix: "Keine Konten" nennt jetzt die Ursache

Drei verschiedene Faelle sahen alle gleich aus: Die Bank schickte nichts, sie
schickte Eintraege ohne Kennung (die still verworfen wurden), oder alle Konten
waren schon bekannt – Letzteres meldete faelschlich "0 Konten", obwohl alles
in Ordnung war.

Die Antwort unterscheidet jetzt uebermittelt / neu / bekannt / ohne_kennung.
Die Rueckkehr-Seite zeigt bei null nutzbaren Konten den passenden Hinweis: Im
eingeschraenkten Betrieb gibt eine Bank nur heraus, was zuvor im Control Panel
unter "Activate by linking accounts" verknuepft wurde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-26 22:32:23 +02:00
co-authored by Claude Opus 5
parent 8fa4ac5ef4
commit 277fe8b0e2
5 changed files with 81 additions and 10 deletions
+18 -4
View File
@@ -188,10 +188,15 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict
except ValueError: except ValueError:
verbindung.valid_until = None verbindung.valid_until = None
neu = 0 uebermittelt = daten.get("accounts") or []
for konto in daten.get("accounts", []): neu = bekannt = ohne_kennung = 0
for konto in uebermittelt:
uid = konto.get("uid") uid = konto.get("uid")
if not 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 continue
vorhanden = ( vorhanden = (
db.query(BankAccountLink) db.query(BankAccountLink)
@@ -199,7 +204,9 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict
.first() .first()
) )
if vorhanden is not None: if vorhanden is not None:
# Schon bekannt – die Zuordnung bleibt bewusst erhalten.
vorhanden.connection_id = verbindung.id vorhanden.connection_id = verbindung.id
bekannt += 1
continue continue
db.add(BankAccountLink( db.add(BankAccountLink(
owner_id=user.id, owner_id=user.id,
@@ -212,8 +219,15 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict
neu += 1 neu += 1
db.commit() db.commit()
return {"connection_id": verbindung.id, "konten": neu, return {
"valid_until": verbindung.valid_until.isoformat() if verbindung.valid_until else None} "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: def _betrag_cents(umsatz: dict) -> int | None:
+39
View File
@@ -480,3 +480,42 @@ def test_genauer_name_schlaegt_teiltreffer(admin, monkeypatch):
monkeypatch.setattr(bank_api.BankClient, "_anfrage", _anfrage) monkeypatch.setattr(bank_api.BankClient, "_anfrage", _anfrage)
namen = [b["name"] for b in admin.get("/api/bank/banks?search=revolut").json()["treffer"]] namen = [b["name"] for b in admin.get("/api/bank/banks?search=revolut").json()["treffer"]]
assert namen == ["Revolut", "Revolut Bank UAB", "Banca Revoluta"] 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
+5 -2
View File
@@ -587,7 +587,7 @@
"connect": "Verbinden", "connect": "Verbinden",
"notMapped": "noch keinem Konto zugeordnet", "notMapped": "noch keinem Konto zugeordnet",
"mapHint": "Ohne Zuordnung wird von diesem Konto nichts abgerufen.", "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", "sync": "Umsätze holen",
"synced": "{{geholt}} Umsätze geprüft · {{neu}} neu im Eingang.", "synced": "{{geholt}} Umsätze geprüft · {{neu}} neu im Eingang.",
"noMapping": "Kein Konto zugeordnet – ordne oben zuerst zu, dann wird abgerufen.", "noMapping": "Kein Konto zugeordnet – ordne oben zuerst zu, dann wird abgerufen.",
@@ -620,6 +620,9 @@
"failed": "Das hat nicht geklappt", "failed": "Das hat nicht geklappt",
"failedHint": "Der Code von der Bank gilt nur einmal und nur kurz. Starte die Verbindung in den Einstellungen neu.", "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.", "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."
} }
} }
+5 -2
View File
@@ -587,7 +587,7 @@
"connect": "Connect", "connect": "Connect",
"notMapped": "not mapped to an account yet", "notMapped": "not mapped to an account yet",
"mapHint": "Without a mapping nothing is fetched from this account.", "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", "sync": "Fetch transactions",
"synced": "{{geholt}} transactions checked · {{neu}} new in the inbox.", "synced": "{{geholt}} transactions checked · {{neu}} new in the inbox.",
"noMapping": "No account mapped – map one above first, then it will fetch.", "noMapping": "No account mapped – map one above first, then it will fetch.",
@@ -620,6 +620,9 @@
"failed": "That did not work", "failed": "That did not work",
"failedHint": "The code from the bank is single-use and short-lived. Start the connection again in the settings.", "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.", "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."
} }
} }
+14 -2
View File
@@ -34,7 +34,7 @@ export default function BankRueckkehr() {
return; return;
} }
api.bankCallback(state, code) 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 })); .catch((e) => setStand({ laeuft: false, fehler: e.message }));
// Nur einmal ausführen: Der Code lässt sich kein zweites Mal einlösen. // Nur einmal ausführen: Der Code lässt sich kein zweites Mal einlösen.
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -56,13 +56,25 @@ export default function BankRueckkehr() {
</div> </div>
)} )}
{!stand.laeuft && !stand.fehler && ( {!stand.laeuft && !stand.fehler && stand.konten > 0 && (
<div className="alert" style={{ display: "block" }}> <div className="alert" style={{ display: "block" }}>
<strong>{t("bankReturn.ok", { n: stand.konten })}</strong> <strong>{t("bankReturn.ok", { n: stand.konten })}</strong>
<p className="muted small">{t("bankReturn.next")}</p> <p className="muted small">{t("bankReturn.next")}</p>
</div> </div>
)} )}
{!stand.laeuft && !stand.fehler && stand.konten === 0 && (
// Drei verschiedene Ursachen sahen bisher alle gleich aus.
<div className="alert error" style={{ display: "block" }}>
<strong>{t("bankReturn.noAccounts")}</strong>
<p className="muted small">
{stand.ohne_kennung > 0
? t("bankReturn.noAccountsDropped", { n: stand.ohne_kennung })
: t("bankReturn.noAccountsHint")}
</p>
</div>
)}
<div className="btn-pair" style={{ marginTop: "var(--sp-3)" }}> <div className="btn-pair" style={{ marginTop: "var(--sp-3)" }}>
<Link className="btn primary" to="/settings"> <Link className="btn primary" to="/settings">
<Icon name="settings" size={15} /> {t("bankReturn.toSettings")} <Icon name="settings" size={15} /> {t("bankReturn.toSettings")}