feat: Pruefskript sagt, WELCHE der beiden Zeilen weg muss

Es liess die Entscheidung beim Nutzer, obwohl die Datenbank sie hergibt: In
bank_account_links steht, welche Konten Certain selbst abholt. Sind beide
angebunden, liefert die Bank beide Seiten und der Uebertrag ist falsch. Wird
das Gegenkonto nicht abgerufen, ist der Uebertrag richtig und die zusaetzliche
Buchung ueberzaehlig. Das steht jetzt je Fall dabei.

Ausserdem war die Zusammenfassung irrefuehrend: Sie addierte Betraege in beide
Richtungen. Eine zusaetzliche Gutschrift macht das Konto zu hoch, eine
zusaetzliche Belastung zu niedrig – aus '2'026 zu viel' werden so '1'125 zu
hoch'.

Dazu zwei Tests zu der Frage, ob der naechste Abruf alles wieder anlegt: Nein.
Der Umsatz wird an der Kennung der Bank wiedererkannt, unabhaengig davon, was
aus der Buchung geworden ist, und ein spaeterer CSV-Auszug faellt ueber den
Eingangs-Abgleich. Aufraeumen haelt also.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-27 11:40:38 +02:00
co-authored by Claude Opus 5
parent 0cd0aab90d
commit 0288b2be9f
2 changed files with 95 additions and 18 deletions
+58
View File
@@ -928,3 +928,61 @@ def test_ohne_angabe_wird_nichts_geraten(admin, fake_bank):
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
assert eintrag["fx_rate"] is None
assert eintrag["counter_amount_cents"] is None
def test_geloeschte_buchung_kommt_beim_naechsten_abruf_nicht_zurueck(admin, fake_bank):
"""Die Frage beim Aufraeumen: Legt der naechste Abruf alles wieder an?
Nein – der Umsatz wird an der Kennung der Bank wiedererkannt, unabhaengig
davon, was aus der Buchung geworden ist. Das ist Absicht: Wer etwas
weggeworfen hat, will es nicht beim naechsten Abruf zurueck.
"""
heute = date.today().isoformat()
verbindung, link, _ = _verbunden(admin, fake_bank,
umsaetze=[dict(UMSATZ, booking_date=heute)])
konto = _konto(admin, name=f"Aufraeumen {secrets.token_hex(3)}", currency="EUR")
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
admin.post(f"/api/bank/connections/{verbindung}/sync")
eintrag = [e for e in admin.get("/api/inbox").json()
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -1490
# So raeumt man auf: die Buchung loeschen.
assert admin.delete(f"/api/transactions/{eintrag['transaction_id']}").status_code == 204
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 0
# Und jetzt derselbe Umsatz noch einmal von der Bank.
zweiter = admin.post(f"/api/bank/connections/{verbindung}/sync").json()
assert zweiter["geholt"] >= 1, "der Umsatz kam wieder an"
assert zweiter["neu"] == 0, "wurde aber nicht erneut angelegt"
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 0
def test_geloeschte_buchung_blockiert_auch_den_csv_import(admin, fake_bank):
"""Der andere Weg herein: Ein spaeterer Auszug darf sie auch nicht bringen."""
heute = date.today()
verbindung, link, _ = _verbunden(
admin, fake_bank, umsaetze=[dict(UMSATZ, booking_date=heute.isoformat())])
konto = _konto(admin, name=f"CSV-Schutz {secrets.token_hex(3)}", currency="EUR")
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
admin.post(f"/api/bank/connections/{verbindung}/sync")
eintrag = [e for e in admin.get("/api/inbox").json()
if e["payee"] == "SPEEDEE RESTAURATIONS"][0]
admin.delete(f"/api/transactions/{eintrag['transaction_id']}")
from app.database import SessionLocal
from app.models import Account, User
from app.services import csv_import
db = SessionLocal()
try:
nutzer = db.query(User).order_by(User.id).first()
acc = db.get(Account, konto["id"])
zeilen = csv_import.detect_rows(db, nutzer, acc, [{
"date": heute.isoformat(), "amount_cents": -1490,
"payee": "SPEEDEE RESTAURATIONS ZUERICH", "purpose": None}])
finally:
db.close()
assert zeilen[0]["is_duplicate"] is True
assert zeilen[0]["duplicate_reason"] == "inbox"