diff --git a/backend/app/services/bank_sync.py b/backend/app/services/bank_sync.py index b2c181e..06c3906 100644 --- a/backend/app/services/bank_sync.py +++ b/backend/app/services/bank_sync.py @@ -651,20 +651,31 @@ def hole_umsaetze(db: Session, user: User, verbindung: BankConnection, # Eingang und niemand weiss, warum. vorgemerkt += 1 continue - kennung = (umsatz.get("entry_reference") or umsatz.get("transaction_id") - or umsatz.get("uid")) - if not kennung: + # ALLE Kennungen, die der Umsatz mitbringt – nicht nur die erste. + # Liefert die Bank denselben Umsatz spaeter unter einer anderen + # (etwa weil aus vorgemerkt gebucht wurde), waeren es sonst zwei. + kennungen = [ + f"{verknuepfung.remote_uid}:{wert}"[:255] + # Reihenfolge nach Bestaendigkeit: Die eigene Kennung der + # Schnittstelle bleibt gleich, wenn die Bank ihre Referenz beim + # Buchen neu vergibt. Gespeichert wird die erste – geprueft + # wird gegen alle, damit auch aeltere Eintraege noch treffen. + for feld in ("transaction_id", "internal_transaction_id", + "entry_reference", "uid") + if (wert := umsatz.get(feld)) + ] + if not kennungen: continue # ohne Kennung kein Schutz vor Doppelabruf - kennung = f"{verknuepfung.remote_uid}:{kennung}"[:255] schon = ( db.query(InboxItem.id) .filter(InboxItem.owner_id == user.id, InboxItem.source == InboxSource.bank, - InboxItem.external_id == kennung) + InboxItem.external_id.in_(kennungen)) .first() ) if schon is not None: continue + kennung = kennungen[0] cents = _betrag_cents(umsatz) tag = _datum(umsatz) diff --git a/backend/tests/test_bank.py b/backend/tests/test_bank.py index 6e1bfe7..6f79b13 100644 --- a/backend/tests/test_bank.py +++ b/backend/tests/test_bank.py @@ -1307,3 +1307,42 @@ def test_zu_knapper_takt_wird_gebremst(): # Nach sechs Stunden schon. knapp.last_sync_at = jetzt - timedelta(hours=7) assert bank_sync.faellig(knapp, jetzt) is True + + +def test_umsatz_unter_anderer_kennung_kommt_nicht_zweimal(admin, fake_bank): + """Liefert die Bank denselben Umsatz spaeter unter einer anderen Kennung – + etwa weil aus vorgemerkt gebucht wurde –, waeren es fuer Certain zwei. + Geprueft wird deshalb gegen ALLE Kennungen, die er mitbringt.""" + _app(admin) + heute = date.today().isoformat() + uid = f"acc-{secrets.token_hex(4)}" + stabil = f"tx-{secrets.token_hex(5)}" + salden = [{"balance_type": "CLBD", + "balance_amount": {"amount": "0.00", "currency": "EUR"}}] + + def umsatz(referenz): + return {"entry_reference": referenz, "transaction_id": stabil, + "booking_date": heute, + "transaction_amount": {"amount": "288.00", "currency": "EUR"}, + "credit_debit_indicator": "DBIT", + "creditor": {"name": "Noemi Koenig"}} + + fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, + umsaetze=[umsatz("ref-vorgemerkt")]) + state = admin.post("/api/bank/connect", + json={"aspsp_name": f"Kennung-{secrets.token_hex(3)}", + "country": "DE"}).json()["state"] + admin.post(f"/api/bank/callback?state={state}&code=guter-code") + link = [v for v in admin.get("/api/bank/links").json() if v["remote_uid"] == uid][0] + konto = _konto(admin, name=f"Kennung {secrets.token_hex(3)}", currency="EUR") + admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}") + + assert admin.post(f"/api/bank/links/{link['id']}/sync").json()["neu"] == 1 + assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -28800 + + # Zweiter Abruf: andere entry_reference, dieselbe transaction_id. + fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, + umsaetze=[umsatz("ref-gebucht")]) + zweiter = admin.post(f"/api/bank/links/{link['id']}/sync").json() + assert zweiter["neu"] == 0, "unter neuer Referenz noch einmal gebucht" + assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -28800