From 14254b483e3aa5164f5e7ddc875c1fe991a7e551 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 19 Oct 2026 06:53:47 +0200 Subject: [PATCH] fix: Die Meldung sagt jetzt, wenn die Gegenseite schon anderweitig verbunden ist Co-Authored-By: Claude Opus 5 --- backend/app/services/uebertrag_buchen.py | 14 ++++++ backend/tests/test_keywords.py | 58 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/backend/app/services/uebertrag_buchen.py b/backend/app/services/uebertrag_buchen.py index 96b6b52..1038955 100644 --- a/backend/app/services/uebertrag_buchen.py +++ b/backend/app/services/uebertrag_buchen.py @@ -173,6 +173,20 @@ def _pruefe_gegenseite(db: Session, user: User, konto: Account, ziel: Account, datum, exakt) if schon is None: return + # Haengt die gefundene Buchung schon an einer anderen, ist sie NICHT die + # Gegenseite dieser hier. Der Rat „verbinden" ginge dann ins Leere: Die + # Vorschlagsliste laesst vergebene Buchungen aus, es kaeme nie ein + # Vorschlag – und der Nutzer sucht etwas, das es nicht gibt. + if _schon_verknuepft(db, user, schon): + raise UebertragFehler( + f"Auf '{ziel.name}' steht zwar eine passende Buchung vom " + f"{schon.date.strftime('%d.%m.%Y')}, die ist aber schon mit einer " + f"anderen Buchung verbunden – also nicht die Gegenseite dieser " + f"hier. Verbinden geht deshalb nicht. Sieh auf '{ziel.name}' nach, " + f"ob die vorhandene Verknüpfung stimmt, oder ob der zweite Zugang " + f"dort schlicht fehlt.", + nachfragen=True, + ) raise UebertragFehler( f"Auf '{ziel.name}' steht schon eine passende Buchung vom " f"{schon.date.strftime('%d.%m.%Y')}. Beide Seiten sind also bereits " diff --git a/backend/tests/test_keywords.py b/backend/tests/test_keywords.py index e8536f6..79a46e3 100644 --- a/backend/tests/test_keywords.py +++ b/backend/tests/test_keywords.py @@ -455,3 +455,61 @@ def test_verknuepfte_buchungen_landen_nicht_in_uebersprungen(admin): # Und sie bleibt unangetastet: kein Uebertrag, kein verschobenes Geld. assert admin.get(f"/api/transactions/{raus['id']}").json()["type"] == "expense" assert admin.get(f"/api/accounts/{pocket['id']}").json()["balance_cents"] == betrag + + +def _tx(admin, konto, typ, datum, cents, payee): + return admin.post("/api/transactions", json={ + "type": typ, "date": datum, "account_id": konto["id"], + "amount_cents": cents, "payee": payee}).json() + + +def test_vergebene_gegenseite_wird_als_solche_benannt(admin): + """Die Pruefung fragt „gibt es auf dem Zielkonto eine passende Buchung?", + nicht „ist sie noch frei?". Haengt die gefundene schon an einer anderen + Buchung, ist sie NICHT die Gegenseite dieser hier – und der Rat „verbinden" + geht ins Leere, denn die Vorschlagsliste laesst vergebene Buchungen aus. + Dann muss die Meldung sagen, was wirklich los ist.""" + quelle = _kto(admin, name=f"Revolut {secrets.token_hex(3)}") + pocket = _kto(admin, name=f"Sparen Pocket {secrets.token_hex(3)}") + anderes = _kto(admin, name=f"Anderes {secrets.token_hex(3)}") + kennung = f"um chf sparen {secrets.token_hex(6)}" + betrag = 62 + secrets.randbelow(900) + + # Der Zugang im Pocket gehoert schon zu einer Buchung vom dritten Konto. + zugang = _tx(admin, pocket, "income", "2026-05-20", betrag, "Pocket") + fremd = _tx(admin, anderes, "expense", "2026-05-20", betrag, "Fremd") + assert admin.post("/api/uebertraege/verbinden", + json={"a_id": fremd["id"], "b_id": zugang["id"]}).status_code == 200 + + # Und jetzt kommt die Revolut-Ausgabe, gleicher Tag, gleicher Betrag. + raus = _tx(admin, quelle, "expense", "2026-05-20", betrag, kennung.upper()) + assert admin.post("/api/categories/transfer-keywords", json={ + "word": kennung, "target_account_id": pocket["id"]}).status_code == 201 + + r = admin.post("/api/categories/keywords/anwenden", params={"vorschau": False}).json() + grund = next(z["grund"] for z in r["uebersprungen"] if z["text"] == kennung.upper()) + assert "schon mit einer anderen Buchung verbunden" in grund + assert "Richtig ist hier, die beiden zu verbinden" not in grund, \ + "zum Verbinden raten waere hier falsch – die Gegenseite ist vergeben" + + # Geblockt wird weiter: kein Uebertrag, kein verschobenes Geld. + assert admin.get(f"/api/transactions/{raus['id']}").json()["type"] == "expense" + assert admin.get(f"/api/accounts/{pocket['id']}").json()["balance_cents"] == betrag + + +def test_freie_gegenseite_raet_weiter_zum_verbinden(admin): + """Die Gegenprobe: Ist die Gegenseite noch frei, bleibt es beim alten Rat. + Sonst schluckte der neue Zweig alle Faelle.""" + quelle = _kto(admin, name=f"Revolut {secrets.token_hex(3)}") + pocket = _kto(admin, name=f"Sparen Pocket {secrets.token_hex(3)}") + kennung = f"um chf sparen {secrets.token_hex(6)}" + betrag = 62 + secrets.randbelow(900) + + _tx(admin, pocket, "income", "2026-05-20", betrag, "Pocket") + _tx(admin, quelle, "expense", "2026-05-20", betrag, kennung.upper()) + assert admin.post("/api/categories/transfer-keywords", json={ + "word": kennung, "target_account_id": pocket["id"]}).status_code == 201 + + r = admin.post("/api/categories/keywords/anwenden", params={"vorschau": False}).json() + grund = next(z["grund"] for z in r["uebersprungen"] if z["text"] == kennung.upper()) + assert "Richtig ist hier, die beiden zu verbinden" in grund