fix: Bankabruf verschluckte Umsaetze mit wiederkehrendem Betrag
Der Eingangs-Abgleich vergleicht Betrag und Datum mit ein paar Tagen Spielraum – bewusst unscharf, damit eine per Kurzbefehl erfasste Zahlung nicht ein zweites Mal ankommt. Er merkt sich dabei, welchen Eintrag er schon verbraucht hat. Nur ruft der Bankabruf ihn JE ZEILE einzeln auf, und damit begann diese Menge jedes Mal von vorn: Derselbe Eintrag liess sich beliebig oft verbrauchen. Bei Revolut-Aufrundungen – dreimal 1.00 in drei Tagen – kam so nur die erste an, und der Saldo wich ab, ohne dass irgendwo etwas stand. Zwei Aenderungen: - Der Aufrufer kann die Menge jetzt ueber mehrere Aufrufe mitfuehren; der Bankabruf haelt eine je Konto. - Gegen andere BANK-Eintraege wird gar nicht mehr unscharf verglichen. Zwei Umsaetze derselben Quelle sind nie Dubletten voneinander – dort entscheidet die Kennung der Bank, und die ist exakt. Dazu ein Weg zurueck: Ein verschluckter Umsatz hinterliess keinen Eintrag, der naechste Abruf haette ihn also wieder geholt – nur liegt er dann vor dem Fenster des letzten Abrufs. "Aelteres nachholen" gibt es jetzt auch je Konto, wie beim Postfach. Nachgestellt und als Test festgehalten: dreimal 1.00 in drei Tagen, alle drei kommen an, Saldo -3.00. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -986,3 +986,70 @@ def test_geloeschte_buchung_blockiert_auch_den_csv_import(admin, fake_bank):
|
||||
db.close()
|
||||
assert zeilen[0]["is_duplicate"] is True
|
||||
assert zeilen[0]["duplicate_reason"] == "inbox"
|
||||
|
||||
|
||||
def test_zwei_gleiche_betraege_kurz_hintereinander_kommen_beide_an(admin, fake_bank):
|
||||
"""Revolut-Aufrundungen sind oft mehrfach derselbe Betrag in wenigen Tagen.
|
||||
Der Eingangs-Abgleich vergleicht nur Betrag und Datum (±3 Tage) – wenn er
|
||||
denselben Eintrag mehrfach verbraucht, faellt die zweite Zahlung weg und
|
||||
der Saldo weicht ab."""
|
||||
_app(admin)
|
||||
heute = date.today()
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
umsaetze = [
|
||||
{"entry_reference": f"tx-{secrets.token_hex(4)}",
|
||||
"booking_date": (heute - timedelta(days=tage)).isoformat(),
|
||||
"transaction_amount": {"amount": "1.00", "currency": "CHF"},
|
||||
"credit_debit_indicator": "DBIT",
|
||||
"creditor": {"name": "To CHF MB:8f33c0aa"}}
|
||||
for tage in (2, 1, 0)
|
||||
]
|
||||
fake_bank(konten=[{"uid": uid, "currency": "CHF"}], umsaetze=umsaetze)
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Rundung-{secrets.token_hex(3)}",
|
||||
"country": "CH"}).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"Rundung {secrets.token_hex(3)}", currency="CHF")
|
||||
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
|
||||
|
||||
r = admin.post(f"/api/bank/links/{link['id']}/sync").json()
|
||||
assert r["geholt"] == 3
|
||||
assert r["neu"] == 3, f"nur {r['neu']} von 3 angekommen"
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -300
|
||||
|
||||
|
||||
def test_verschluckter_umsatz_laesst_sich_nachholen(admin, fake_bank):
|
||||
"""Wurde etwas faelschlich als Dublette uebersprungen, entstand kein
|
||||
Eingangs-Eintrag – der naechste Abruf haette es also wieder geholt. Nur
|
||||
liegt es dann meist vor dem Fenster des letzten Abrufs."""
|
||||
_app(admin)
|
||||
alt = (date.today() - timedelta(days=40)).isoformat()
|
||||
uid = f"acc-{secrets.token_hex(4)}"
|
||||
gegenstelle = fake_bank(
|
||||
konten=[{"uid": uid, "currency": "EUR"}],
|
||||
umsaetze=[dict(UMSATZ, entry_reference=f"tx-{secrets.token_hex(4)}",
|
||||
booking_date=alt)])
|
||||
state = admin.post("/api/bank/connect",
|
||||
json={"aspsp_name": f"Nachhol-{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"Nachhol {secrets.token_hex(3)}", currency="EUR")
|
||||
# Eine vorhandene Buchung schiebt das Startdatum nach vorn.
|
||||
admin.post("/api/transactions", json={
|
||||
"type": "expense", "date": date.today().isoformat(), "account_id": konto["id"],
|
||||
"amount_cents": 100, "payee": "schon da"})
|
||||
admin.post(f"/api/bank/links/{link['id']}/account/{konto['id']}")
|
||||
|
||||
# Der normale Abruf greift nicht weit genug zurueck.
|
||||
assert admin.post(f"/api/bank/links/{link['id']}/sync").json()["neu"] == 0
|
||||
assert gegenstelle.date_from > alt
|
||||
|
||||
# Mit Zeitraum schon.
|
||||
r = admin.post(f"/api/bank/links/{link['id']}/sync", params={"tage": 90}).json()
|
||||
assert r["neu"] == 1, r
|
||||
assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -100 - 1490
|
||||
|
||||
assert admin.post(f"/api/bank/links/{link['id']}/sync",
|
||||
params={"tage": 400}).status_code == 400
|
||||
|
||||
Reference in New Issue
Block a user