fix: Die Kennung der Bank steht jetzt an der Buchung selbst
Bisher lag sie nur am Eingangs-Eintrag. Das reicht nicht: Der Eintrag ist eine Zwischenstation und faellt weg, wenn ein Konto geloescht wird oder ein aelteres Backup ihn nicht kannte. Dann war die Zahlung fuer den naechsten Abruf neu, obwohl sie laengst gebucht war. Am schlimmsten bei einem als Uebertrag gebuchten Umsatz: Der ist von der Aehnlichkeitspruefung ausgenommen, dort griff also gar nichts mehr. Die Kennung gehoert deshalb an die Buchung (transactions.source_uid, mit der Quelle davor). Dort ueberlebt sie alles, was mit dem Eingang passiert, und die Eindeutigkeit haelt die Datenbank selbst durch – nicht erst der pruefende Code davor. Gestempelt wird an allen drei Buchungswegen: Eingang, Kurzbefehl und Uebertrag. Vorhandene Buchungen bekommen die Kennung nachgetragen, solange ihr Eingangs-Eintrag noch da ist. Ohne Kennung – von Hand erfasst, aus einer CSV-Datei – bleibt es beim bisherigen Schutz ueber die Aehnlichkeit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
175d9f50e0
commit
991a8e4fc2
@@ -1346,3 +1346,130 @@ def test_umsatz_unter_anderer_kennung_kommt_nicht_zweimal(admin, fake_bank):
|
||||
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
|
||||
|
||||
|
||||
# --- Die Kennung steht an der Buchung ---------------------------------------
|
||||
|
||||
|
||||
def _eingaenge_loeschen(uid: str) -> int:
|
||||
"""Alle Eingangs-Eintraege dieses Bankkontos entfernen.
|
||||
|
||||
Stellt nach, was im Betrieb wirklich passiert: Der Eintrag ist eine
|
||||
Zwischenstation und faellt weg – geloeschtes Konto, aelteres Backup,
|
||||
neu verbundene Bank mit neuer Kennung. Die Buchung bleibt.
|
||||
"""
|
||||
from app.database import SessionLocal
|
||||
from app.models import InboxItem
|
||||
|
||||
with SessionLocal() as s:
|
||||
eintraege = s.query(InboxItem).filter(
|
||||
InboxItem.external_id.like(f"{uid}:%")).all()
|
||||
for e in eintraege:
|
||||
s.delete(e)
|
||||
s.commit()
|
||||
return len(eintraege)
|
||||
|
||||
|
||||
def test_kennung_der_bank_steht_an_der_buchung(admin, fake_bank):
|
||||
"""Nicht nur am Eingangs-Eintrag. Nur an der Buchung ueberlebt sie alles,
|
||||
was mit dem Eingang passieren kann."""
|
||||
from app.database import SessionLocal
|
||||
from app.models import Transaction
|
||||
|
||||
_app(admin)
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
tx = f"tx-{secrets.token_hex(5)}"
|
||||
fake_bank(konten=[{"uid": uid, "currency": "EUR"}],
|
||||
salden=[{"balance_type": "CLBD",
|
||||
"balance_amount": {"amount": "0.00", "currency": "EUR"}}],
|
||||
umsaetze=[{"transaction_id": tx, "booking_date": date.today().isoformat(),
|
||||
"transaction_amount": {"amount": "31.40", "currency": "EUR"},
|
||||
"credit_debit_indicator": "DBIT",
|
||||
"creditor": {"name": "Baecker Ohnesorg"}}])
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Steht-{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"Steht {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
|
||||
|
||||
with SessionLocal() as s:
|
||||
txn = s.query(Transaction).filter(Transaction.account_id == konto["id"]).one()
|
||||
assert txn.source_uid == f"bank:{uid}:{tx}"
|
||||
|
||||
|
||||
def test_ohne_eingangs_eintrag_kommt_der_umsatz_nicht_zurueck(admin, fake_bank):
|
||||
"""Faellt der Eingangs-Eintrag weg, blieb bisher nur noch die unscharfe
|
||||
Aehnlichkeit. Jetzt erkennt die Kennung an der Buchung den Umsatz exakt
|
||||
wieder – daran, dass ``doppelt`` bei 0 bleibt, sieht man den Unterschied."""
|
||||
_app(admin)
|
||||
heute = date.today().isoformat()
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
salden = [{"balance_type": "CLBD",
|
||||
"balance_amount": {"amount": "0.00", "currency": "EUR"}}]
|
||||
umsatz = {"transaction_id": f"tx-{secrets.token_hex(5)}", "booking_date": heute,
|
||||
"transaction_amount": {"amount": "76.20", "currency": "EUR"},
|
||||
"credit_debit_indicator": "DBIT",
|
||||
"creditor": {"name": "Werkzeug Nadolny"}}
|
||||
|
||||
fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, umsaetze=[umsatz])
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Ohne-{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"Ohne {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 _eingaenge_loeschen(uid) == 1
|
||||
|
||||
fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, umsaetze=[umsatz])
|
||||
zweiter = admin.post(f"/api/bank/links/{link['id']}/sync").json()
|
||||
assert zweiter["neu"] == 0, "ohne Eingangs-Eintrag noch einmal gebucht"
|
||||
assert zweiter["doppelt"] == 0, "nur noch unscharf erkannt statt an der Kennung"
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -7620
|
||||
|
||||
|
||||
def test_als_uebertrag_gebuchter_umsatz_kommt_nicht_zurueck(admin, fake_bank):
|
||||
"""Der Fall, in dem sonst NICHTS mehr greift.
|
||||
|
||||
Ein Uebertrag ist von der Aehnlichkeitspruefung ausgenommen (er hat kein
|
||||
Gegenueber in der Ausgaben-/Einnahmen-Liste), und ohne Eingangs-Eintrag
|
||||
gibt es auch dort nichts zu finden. Bleibt allein die Kennung.
|
||||
"""
|
||||
_app(admin)
|
||||
heute = date.today().isoformat()
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
salden = [{"balance_type": "CLBD",
|
||||
"balance_amount": {"amount": "0.00", "currency": "EUR"}}]
|
||||
umsatz = {"transaction_id": f"tx-{secrets.token_hex(5)}", "booking_date": heute,
|
||||
"transaction_amount": {"amount": "500.00", "currency": "EUR"},
|
||||
"credit_debit_indicator": "DBIT",
|
||||
"creditor": {"name": "Eigenes Sparkonto"}}
|
||||
|
||||
fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, umsaetze=[umsatz])
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Uebertrag-{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"Uebertrag {secrets.token_hex(3)}", currency="EUR")
|
||||
sparkonto = _konto(admin, name=f"Sparen {secrets.token_hex(3)}", currency="EUR",
|
||||
type="sparkonto")
|
||||
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
|
||||
assert admin.post(f"/api/bank/links/{link['id']}/sync").json()["neu"] == 1
|
||||
|
||||
gebucht = admin.get(f"/api/transactions?account_id={konto['id']}").json()[0]
|
||||
r = admin.post(f"/api/transactions/{gebucht['id']}/transfer",
|
||||
json={"to_account_id": sparkonto["id"], "trotzdem": True})
|
||||
assert r.status_code == 200, r.text
|
||||
assert _eingaenge_loeschen(uid) >= 1
|
||||
|
||||
fake_bank(konten=[{"uid": uid, "currency": "EUR"}], salden=salden, umsaetze=[umsatz])
|
||||
zweiter = admin.post(f"/api/bank/links/{link['id']}/sync").json()
|
||||
assert zweiter["neu"] == 0, "als Uebertrag gebucht und trotzdem neu geholt"
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -50000
|
||||
|
||||
Reference in New Issue
Block a user