feat: Bank ueber alle Laender suchen

Unter "Deutschland" liessen sich N26 und Revolut nicht finden – und bei
Revolut kann das gar nicht klappen: Die Bank vergibt deutsche IBANs, ist aber
die Revolut Bank UAB in Litauen. Das Sitzland einer Bank steht nicht in der
IBAN, also ist die Laenderauswahl der falsche Einstieg.

Jetzt ein Suchfeld, das ohne Land ueber alle sucht; das Land ist nur noch ein
optionaler Filter. Der Treffer zeigt sein Land mit an, und verbunden wird mit
dem Land der BANK statt dem des Filters – sonst schluege die Autorisierung
fehl, sobald ohne Land gesucht wurde.

Ohne Land UND ohne Begriff wird abgelehnt: Sonst kaemen alle Banken Europas
ins Formular.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-26 21:42:08 +02:00
co-authored by Claude Opus 5
parent cba7ec31e3
commit 71e8419634
8 changed files with 134 additions and 38 deletions
+13 -3
View File
@@ -56,11 +56,21 @@ def delete_app(db: Session = Depends(get_db), user: User = Depends(get_current_u
@router.get("/banks")
def list_banks(country: str, db: Session = Depends(get_db),
def list_banks(country: str | None = None, search: str | None = None,
db: Session = Depends(get_db),
user: User = Depends(get_current_user)) -> list[dict]:
"""Verfügbare Banken eines Landes – für die Auswahl beim Verbinden."""
"""Banken suchen – in einem Land oder über alle hinweg.
Ohne ``country`` wird überall gesucht. Das braucht es, weil die IBAN nicht
verrät, wo eine Bank sitzt: Revolut vergibt deutsche IBANs aus Litauen.
"""
if not country and not search:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Bitte ein Land wählen oder einen Suchbegriff eingeben.",
)
try:
return bank_sync.banken(db, user, country)
return bank_sync.banken(db, user, country, search)
except BankFehler as e:
raise _fehler(e) from e
+9 -4
View File
@@ -108,10 +108,15 @@ class BankClient:
# --- Die vier Endpunkte -------------------------------------------------
def banken(self, land: str) -> list[dict]:
"""Verfügbare Banken eines Landes – für die Auswahl beim Verbinden."""
daten = self._anfrage("GET", f"/aspsps?country={land.upper()}")
return daten.get("aspsps", [])
def banken(self, land: str | None = None) -> list[dict]:
"""Verfügbare Banken – eines Landes oder alle.
Ohne Land liefert die Schnittstelle die vollständige Liste. Das ist
nötig, weil das Sitzland einer Bank nicht an der IBAN ablesbar ist:
Revolut gibt deutsche IBANs aus, ist aber in Litauen ansässig.
"""
pfad = f"/aspsps?country={land.upper()}" if land else "/aspsps"
return self._anfrage("GET", pfad).get("aspsps", [])
def starte_autorisierung(self, aspsp_name: str, land: str, redirect_url: str,
state: str, tage: int, psu_type: str = "personal") -> dict:
+25 -7
View File
@@ -60,14 +60,32 @@ def _client(db: Session, user: User) -> BankClient:
return BankClient(app.application_id, schluessel)
def banken(db: Session, user: User, land: str) -> list[dict]:
"""Banken eines Landes, auf das Nötige eingedampft."""
# Nicht die ganze Liste ins Formular kippen – über alle Länder sind es
# tausende. Wer sucht, bekommt Treffer; wer ein Land wählt, dessen Banken.
MAX_TREFFER = 200
def banken(db: Session, user: User, land: str | None = None,
suche: str | None = None) -> list[dict]:
"""Banken suchen – in einem Land oder über alle hinweg.
Die Suche über alle Länder ist kein Luxus: Das Sitzland einer Bank steht
nicht in der IBAN. Revolut gibt deutsche IBANs aus, ist aber in Litauen
ansässig – wer unter „Deutschland" sucht, findet es dort nie.
"""
roh = _client(db, user).banken(land)
return [
{"name": b.get("name"), "country": b.get("country"),
"logo": b.get("logo"), "psu_types": b.get("psu_types", [])}
for b in roh if b.get("name")
]
begriff = (suche or "").strip().lower()
treffer = []
for b in roh:
name = b.get("name")
if not name:
continue
if begriff and begriff not in name.lower():
continue
treffer.append({"name": name, "country": b.get("country"),
"logo": b.get("logo"), "psu_types": b.get("psu_types", [])})
treffer.sort(key=lambda b: (b["name"].lower(), b.get("country") or ""))
return treffer[:MAX_TREFFER]
def starte_verbindung(db: Session, user: User, aspsp_name: str, land: str,
+38 -3
View File
@@ -45,8 +45,12 @@ class FakeBank:
self.jwt = (headers or {}).get("Authorization", "")[7:]
self.aufrufe.append(f"{methode} {pfad.split('?')[0]}")
if pfad.startswith("/aspsps"):
return 200, {"aspsps": [{"name": "N26", "country": "DE"},
{"name": "Revolut", "country": "LT"}]}
alle = [{"name": "N26", "country": "DE"},
{"name": "Revolut", "country": "LT"}]
if "country=" in pfad:
land = pfad.split("country=")[1].split("&")[0]
alle = [b for b in alle if b["country"] == land]
return 200, {"aspsps": alle}
if pfad == "/auth":
return 200, {"url": "https://bank.example/authorize?x=1",
"authorization_id": "auth-1"}
@@ -182,11 +186,12 @@ def test_unbrauchbarer_schluessel_meldet_sich_verstaendlich():
def test_banken_eines_landes_abrufen(admin, fake_bank):
"""Ein Land liefert nur dessen Banken – Revolut sitzt in Litauen."""
_app(admin)
fake_bank()
r = admin.get("/api/bank/banks?country=DE")
assert r.status_code == 200, r.text
assert [b["name"] for b in r.json()] == ["N26", "Revolut"]
assert [b["name"] for b in r.json()] == ["N26"]
def test_verbinden_und_konten_finden(admin, fake_bank):
@@ -358,3 +363,33 @@ def test_ablauf_der_zustimmung_wird_gemeldet(admin, fake_bank):
liste = [c for c in admin.get("/api/bank/connections").json() if c["id"] == verbindung]
assert liste[0]["expires_soon"] is True
def test_suche_findet_ueber_alle_laender(admin, fake_bank):
"""Das Sitzland steht nicht in der IBAN.
Revolut vergibt deutsche IBANs, ist aber in Litauen ansässig. Wer unter
„Deutschland" sucht, findet es dort nie – deshalb muss ohne Land gesucht
werden können.
"""
_app(admin)
fake_bank()
alle = admin.get("/api/bank/banks?search=revolut")
assert alle.status_code == 200, alle.text
treffer = alle.json()
assert [b["name"] for b in treffer] == ["Revolut"]
assert treffer[0]["country"] == "LT"
# Unter Deutschland gesucht liefert dieselbe Bank NICHT.
nur_de = admin.get("/api/bank/banks?country=DE&search=revolut").json()
assert nur_de == []
def test_suche_ohne_land_und_ohne_begriff_wird_abgelehnt(admin, fake_bank):
"""Sonst käme die vollständige Liste aller Banken Europas ins Formular."""
_app(admin)
fake_bank()
r = admin.get("/api/bank/banks")
assert r.status_code == 400
assert "Suchbegriff" in r.json()["detail"]