diff --git a/backend/app/services/bank_sync.py b/backend/app/services/bank_sync.py index 41f91ed..2694161 100644 --- a/backend/app/services/bank_sync.py +++ b/backend/app/services/bank_sync.py @@ -146,11 +146,24 @@ def starte_verbindung(db: Session, user: User, aspsp_name: str, land: str, raise BankFehler("Es ist noch keine Bank-Anwendung hinterlegt.") state = secrets.token_urlsafe(24) - verbindung = BankConnection( - owner_id=user.id, aspsp_name=aspsp_name, aspsp_country=land.upper(), - pending_state=state, + # Dieselbe Bank erneut verbinden frischt den bestehenden Eintrag auf, statt + # einen zweiten anzulegen. Sonst müsste man vorher löschen – und dabei + # gingen die Kontozuordnungen mit verloren, obwohl man sie behalten will. + verbindung = ( + db.query(BankConnection) + .filter(BankConnection.owner_id == user.id, + BankConnection.aspsp_name == aspsp_name, + BankConnection.aspsp_country == land.upper()) + .order_by(BankConnection.id.desc()) + .first() ) - db.add(verbindung) + if verbindung is None: + verbindung = BankConnection( + owner_id=user.id, aspsp_name=aspsp_name, aspsp_country=land.upper(), + ) + db.add(verbindung) + verbindung.pending_state = state + verbindung.last_error = None db.flush() antwort = _client(db, user).starte_autorisierung( diff --git a/backend/tests/test_bank.py b/backend/tests/test_bank.py index c2e430d..b2d6090 100644 --- a/backend/tests/test_bank.py +++ b/backend/tests/test_bank.py @@ -250,8 +250,12 @@ def _verbunden(admin, fake_bank_fn, **kw): "currency": "EUR", "account_id": {"iban": "DE02120300000000202051"}, }]) gegenstelle = fake_bank_fn(**kw) + # Eigener Bankname je Aufruf: Erneutes Verbinden derselben Bank frischt + # bewusst den bestehenden Eintrag auf, sonst erbte jeder Test die + # Verbindung des vorigen. + bank = f"Testbank-{secrets.token_hex(3)}" state = admin.post("/api/bank/connect", - json={"aspsp_name": "N26", "country": "DE"}).json()["state"] + json={"aspsp_name": bank, "country": "DE"}).json()["state"] antwort = admin.post(f"/api/bank/callback?state={state}&code=guter-code").json() link = [v for v in admin.get("/api/bank/links").json() if v["connection_id"] == antwort["connection_id"]][0] @@ -519,3 +523,35 @@ def test_konten_ohne_kennung_werden_gemeldet_statt_verworfen(admin, fake_bank): assert antwort["uebermittelt"] == 1 assert antwort["ohne_kennung"] == 1 assert antwort["konten"] == 0 + + +def test_erneutes_verbinden_behaelt_die_kontozuordnung(admin, fake_bank): + """Ohne das müsste man vor dem Neuverbinden löschen – und verlöre dabei + die Zuordnungen, die man gerade behalten will.""" + _app(admin) + uid = f"acc-{secrets.token_hex(4)}" + konten = [{"uid": uid, "name": "Girokonto", "currency": "EUR", + "account_id": {"iban": "DE02120300000000202051"}}] + fake_bank(konten=konten) + + def verbinde(): + state = admin.post("/api/bank/connect", + json={"aspsp_name": "Testbank", "country": "DE"}).json()["state"] + return admin.post(f"/api/bank/callback?state={state}&code=guter-code").json() + + erste = verbinde() + link = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid][0] + konto = _konto(admin, name="Zuordnung bleibt", currency="EUR") + admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}") + + zweite = verbinde() + assert zweite["connection_id"] == erste["connection_id"], "kein zweiter Eintrag" + + danach = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid] + assert len(danach) == 1 + assert danach[0]["account_id"] == konto["id"], "die Zuordnung muss bleiben" + + # Und es steht genau eine Verbindung zu dieser Bank da. + passende = [c for c in admin.get("/api/bank/connections").json() + if c["aspsp_name"] == "Testbank"] + assert len(passende) == 1 diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 19c301c..3cfae14 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -597,7 +597,7 @@ "validUntil": "Zustimmung gültig bis {{datum}}", "expiresSoon": "Zustimmung läuft am {{datum}} ab – danach musst du neu autorisieren.", "disconnect": "Verbindung trennen", - "disconnectConfirm": "Verbindung zu „{{name}}“ trennen? Bereits abgerufene Buchungen bleiben.", + "disconnectConfirm": "Verbindung zu „{{name}}“ trennen? Dabei gehen die Kontozuordnungen verloren. Zum Auffrischen musst du NICHT trennen – such die Bank einfach erneut und verbinde nochmal, die Zuordnungen bleiben dann.", "search": "Bank suchen", "searchPlaceholder": "z. B. revolut oder n26", "searchHint": "Ohne Land wird überall gesucht. Das ist meist richtig: Das Sitzland einer Bank steht nicht in der IBAN – Revolut vergibt deutsche IBANs, sitzt aber in Litauen.", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index ff69f2c..b12b5d9 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -597,7 +597,7 @@ "validUntil": "Consent valid until {{datum}}", "expiresSoon": "Consent expires on {{datum}} – you will need to authorise again.", "disconnect": "Disconnect", - "disconnectConfirm": "Disconnect from “{{name}}”? Transactions already fetched stay.", + "disconnectConfirm": "Disconnect from “{{name}}”? The account mappings are lost. You do NOT need to disconnect to refresh – just search the bank and connect again, the mappings are kept.", "search": "Search for a bank", "searchPlaceholder": "e.g. revolut or n26", "searchHint": "Without a country it searches everywhere, which is usually right: a bank's home country is not in the IBAN – Revolut issues German IBANs but is based in Lithuania.",