feat: Kontostand von der Bank – die eigene Rechnung wird geprueft
Certain rechnet den Saldo bisher ausschliesslich selbst aus: Anfangsbestand plus alle Buchungen. Stimmt eine Buchung nicht, faellt das erst auf, wenn man von Hand mit dem Auszug vergleicht. Der von der Bank gemeldete Saldo wird jetzt mitgefuehrt und danebengestellt. Weicht er ab, fehlt oder doppelt eine Buchung – und man sieht es auf der Kontenseite und in den Einstellungen sofort, statt es zu suchen. Banken melden mehrere Saldi nebeneinander; ausgewaehlt wird der gebuchte, denn nur der ist mit Certains Rechnung vergleichbar. Ein verfuegbarer Saldo enthaelt Vorgemerktes oder einen Ueberziehungsrahmen. Die Saldi kommen auch fuer noch nicht zugeordnete Konten: Bei mehreren Revolut-Pockets ist der Betrag oft das Einzige, woran man sie unterscheidet. Ein Fehler bei einem Konto beendet den Durchlauf nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,8 +31,11 @@ PEM = SCHLUESSEL.private_bytes(
|
||||
class FakeBank:
|
||||
"""Minimale Gegenstelle: nur was ``BankClient`` tatsächlich aufruft."""
|
||||
|
||||
def __init__(self, umsaetze=None, konten=None, seiten=1):
|
||||
def __init__(self, umsaetze=None, konten=None, seiten=1, salden=None):
|
||||
self.umsaetze = umsaetze or []
|
||||
self.salden = salden if salden is not None else [
|
||||
{"balance_amount": {"amount": "100.00", "currency": "EUR"}},
|
||||
]
|
||||
self.konten = konten if konten is not None else [{
|
||||
"uid": "acc-1", "name": "Girokonto", "currency": "EUR",
|
||||
"account_id": {"iban": "DE02120300000000202051"},
|
||||
@@ -60,8 +63,7 @@ class FakeBank:
|
||||
return 200, {"session_id": "sess-1", "accounts": self.konten,
|
||||
"access": {"valid_until": "2027-01-01T00:00:00+00:00"}}
|
||||
if "/balances" in pfad:
|
||||
return 200, {"balances": [{"balance_amount": {"amount": "100.00",
|
||||
"currency": "EUR"}}]}
|
||||
return 200, {"balances": self.salden}
|
||||
if "/transactions" in pfad:
|
||||
# Blättern nachstellen: erst mit Fortsetzungsschlüssel, dann ohne.
|
||||
hat_schluessel = "continuation_key=" in pfad
|
||||
@@ -611,3 +613,64 @@ def test_merkmale_werden_beim_neuverbinden_aufgefrischt(admin, fake_bank):
|
||||
danach = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid][0]
|
||||
assert danach["product"] == "Savings"
|
||||
assert danach["remote_name"] == "Urlaub"
|
||||
|
||||
|
||||
def test_saldo_der_bank_wird_gefuehrt_und_gebuchter_bevorzugt(admin, fake_bank):
|
||||
"""Banken melden mehrere Saldi nebeneinander. Den ersten zu nehmen waere
|
||||
Zufall – vergleichbar mit Certains eigener Rechnung ist nur der gebuchte."""
|
||||
_app(admin)
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
fake_bank(
|
||||
konten=[{"uid": uid, "name": "Girokonto", "currency": "EUR",
|
||||
"account_id": {"iban": "DE02120300000000202051"}}],
|
||||
salden=[
|
||||
# Verfuegbar steht zuerst und ist hoeher – er enthaelt den
|
||||
# Ueberziehungsrahmen und darf gerade NICHT gewinnen.
|
||||
{"balance_type": "ITAV", "balance_amount": {"amount": "1500.00",
|
||||
"currency": "EUR"}},
|
||||
{"balance_type": "CLBD", "balance_amount": {"amount": "342.15",
|
||||
"currency": "EUR"},
|
||||
"reference_date": "2026-08-25"},
|
||||
],
|
||||
)
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Saldobank-{secrets.token_hex(3)}",
|
||||
"country": "DE"}).json()["state"]
|
||||
# Die Kennung aus der Rueckmeldung nehmen, nicht die zuletzt angelegte
|
||||
# Verbindung: Die Testdatenbank lebt ueber den ganzen Lauf.
|
||||
verbindung_id = admin.post(
|
||||
f"/api/bank/callback?state={state}&code=guter-code").json()["connection_id"]
|
||||
|
||||
r = admin.post(f"/api/bank/connections/{verbindung_id}/salden")
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json()["saldi"] == 1
|
||||
|
||||
link = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid][0]
|
||||
assert link["balance_cents"] == 34215
|
||||
assert link["balance_type"] == "CLBD"
|
||||
assert link["balance_at"].startswith("2026-08-25")
|
||||
|
||||
|
||||
def test_saldo_kommt_auch_ohne_zuordnung(admin, fake_bank):
|
||||
"""Genau dann braucht man ihn: Bei mehreren Pockets ist der Betrag oft das
|
||||
Einzige, woran man erkennt, welches man vor sich hat."""
|
||||
_app(admin)
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
fake_bank(
|
||||
konten=[{"uid": uid, "currency": "CHF", "product": "Steuern"}],
|
||||
salden=[{"balance_type": "CLBD",
|
||||
"balance_amount": {"amount": "2480.00", "currency": "CHF"}}],
|
||||
)
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Revolut-{secrets.token_hex(3)}",
|
||||
"country": "LT"}).json()["state"]
|
||||
verbindung_id = admin.post(
|
||||
f"/api/bank/callback?state={state}&code=guter-code").json()["connection_id"]
|
||||
|
||||
r = admin.post(f"/api/bank/connections/{verbindung_id}/sync")
|
||||
assert r.status_code == 200, r.text
|
||||
# Kein Konto zugeordnet – trotzdem ist der Saldo da.
|
||||
assert r.json()["ohne_zuordnung"] is True
|
||||
assert r.json()["saldi"] == 1
|
||||
link = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid][0]
|
||||
assert link["balance_cents"] == 248000
|
||||
|
||||
Reference in New Issue
Block a user