feat: Rueckkehr von der Bank sagt, was ankam und was liegen blieb

"3 Konten gefunden" beantwortet die entscheidende Frage nicht: Hat die Bank nur
drei geschickt, oder hat Certain die uebrigen verworfen? Der Server rechnet
beides laengst aus, die Seite zeigte es nur, wenn ueberhaupt kein Konto ankam.

Jetzt steht immer dabei, wie viele die Bank geschickt hat. Und Konten, die
Certain nicht uebernehmen konnte, werden nicht mehr bloss gezaehlt, sondern
mitsamt dem gezeigt, was von ihnen ankam – uebersprungen werden sie, weil die
Kennung fehlt, mit der sich Salden und Umsaetze holen lassen.

Anlass: Fuenf Revolut-Pockets sind im Control Panel freigeschaltet und wurden
bei der Autorisierung ausdruecklich mitgewaehlt, kommen in Certain aber nicht
an. Ohne diese Zahlen laesst sich nicht sagen, wo sie verloren gehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 21:12:45 +02:00
co-authored by Claude Opus 5
parent 4c2e72cb20
commit a159546218
5 changed files with 68 additions and 7 deletions
+12 -5
View File
@@ -247,14 +247,15 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict
verbindung.valid_until = None
uebermittelt = daten.get("accounts") or []
neu = bekannt = ohne_kennung = 0
neu = bekannt = 0
uebergangen: list[dict] = []
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
uebergangen.append(konto)
# Nicht stillschweigend verwerfen: Sonst sieht „3 Konten" genauso
# aus, als hätte die Bank nur drei geschickt – und man sucht den
# Fehler bei der Bank statt hier.
ohne_kennung += 1
continue
vorhanden = (
db.query(BankAccountLink)
@@ -283,7 +284,13 @@ def schliesse_verbindung(db: Session, user: User, state: str, code: str) -> dict
"neu": neu,
"bekannt": bekannt,
"uebermittelt": len(uebermittelt),
"ohne_kennung": ohne_kennung,
"ohne_kennung": len(uebergangen),
# Was genau übergangen wurde. Ohne das bleibt die Frage offen, ob die
# Bank ein Konto gar nicht schickt oder Certain es nicht versteht.
"uebergangen": [
{k: v for k, v in konto.items() if k != "identification_hashes"}
for konto in uebergangen[:20]
],
"valid_until": verbindung.valid_until.isoformat() if verbindung.valid_until else None,
}
+28
View File
@@ -1129,3 +1129,31 @@ def test_abruf_ueber_schon_eingelesene_monate_bucht_nicht_doppelt(admin, fake_ba
assert r["neu"] == 1, "der echte Umsatz muss ankommen"
# 35.00 einmal (aus der CSV) plus 7.77 neu – nicht 35.00 zweimal.
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -4277
def test_uebergangene_konten_werden_gezeigt_nicht_nur_gezaehlt(admin, fake_bank):
"""Ohne die Zahl der uebermittelten Konten laesst sich nicht sagen, ob ein
fehlendes Konto gar nicht kam oder hier haengen blieb – und man sucht an
der falschen Stelle."""
_app(admin)
gut = f"acc-{secrets.token_hex(4)}"
fake_bank(konten=[
{"uid": gut, "name": "Girokonto", "currency": "CHF"},
# So sieht ein Pocket aus, wenn die Kennung fehlt.
{"name": "Sparen", "currency": "CHF",
"account_id": {"other": {"identification": "OTHI 6bdb2e3e", "scheme_name": "OTHI"}}},
{"name": "Steuern", "currency": "CHF"},
])
state = admin.post("/api/bank/connect",
json={"aspsp_name": f"Pockets-{secrets.token_hex(3)}",
"country": "LT"}).json()["state"]
r = admin.post(f"/api/bank/callback?state={state}&code=guter-code")
assert r.status_code == 200, r.text
daten = r.json()
assert daten["konten"] == 1
assert daten["uebermittelt"] == 3, "die Bank hat drei geschickt"
assert daten["ohne_kennung"] == 2
# Und zwar mitsamt dem, was ankam – sonst raet man, was ihnen fehlt.
namen = {k.get("name") for k in daten["uebergangen"]}
assert namen == {"Sparen", "Steuern"}
+4 -1
View File
@@ -666,7 +666,10 @@
"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."
"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.",
"uebermittelt": "Die Bank hat {{n}} Konten geschickt.",
"skipped": "{{n}} Konten konnte Certain nicht übernehmen",
"skippedHint": "Ihnen fehlt die Kennung, mit der Salden und Umsätze geholt werden. Das steht unten so, wie es angekommen ist – schick es mir, dann sehe ich, was fehlt."
},
"transfers": {
"title": "Mögliche Überträge",
+4 -1
View File
@@ -666,7 +666,10 @@
"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."
"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.",
"uebermittelt": "The bank sent {{n}} accounts.",
"skipped": "{{n}} accounts could not be taken over",
"skippedHint": "They lack the identifier used to fetch balances and transactions. Below is what arrived, unchanged."
},
"transfers": {
"title": "Possible transfers",
+20
View File
@@ -59,10 +59,30 @@ export default function BankRueckkehr() {
{!stand.laeuft && !stand.fehler && stand.konten > 0 && (
<div className="alert" style={{ display: "block" }}>
<strong>{t("bankReturn.ok", { n: stand.konten })}</strong>
{/* Wie viele die Bank GESCHICKT hat. Ohne diese Zahl lässt sich
nicht sagen, ob ein fehlendes Konto gar nicht kam oder hier
hängen blieb – und man sucht an der falschen Stelle. */}
<p className="muted small">
{t("bankReturn.uebermittelt", { n: stand.uebermittelt })}
</p>
<p className="muted small">{t("bankReturn.next")}</p>
</div>
)}
{!stand.laeuft && (stand.uebergangen || []).length > 0 && (
<div className="alert warn" style={{ display: "block" }}>
<strong>{t("bankReturn.skipped", { n: stand.uebergangen.length })}</strong>
<p className="muted small">{t("bankReturn.skippedHint")}</p>
<div className="selectable small muted">
{stand.uebergangen.map((k, i) => (
<pre key={i} style={{ whiteSpace: "pre-wrap", margin: "4px 0" }}>
{JSON.stringify(k)}
</pre>
))}
</div>
</div>
)}
{!stand.laeuft && !stand.fehler && stand.konten === 0 && (
// Drei verschiedene Ursachen sahen bisher alle gleich aus.
<div className="alert error" style={{ display: "block" }}>