import io import json def _konto(admin, **kw): body = {"name": "Giro", "type": "girokonto", "currency": "EUR", "opening_balance_cents": 0} body.update(kw) return admin.post("/api/accounts", json=body).json() def _kategorie(admin): for g in admin.get("/api/categories").json(): for c in g["categories"]: if c["name"] == "Lebensmittel": return c["id"] raise AssertionError("Lebensmittel fehlt") def test_offene_buchungen_listet_nur_unkategorisierte(admin): """Zum Abarbeiten braucht man die Buchungen ohne Kategorie – und zwar nur die.""" konto = _konto(admin, name="Abarbeiten") kat = _kategorie(admin) admin.post("/api/transactions", json={"type": "expense", "date": "2026-04-01", "account_id": konto["id"], "amount_cents": 1000, "payee": "Ohne Kategorie"}) admin.post("/api/transactions", json={"type": "expense", "date": "2026-04-02", "account_id": konto["id"], "amount_cents": 2000, "payee": "Mit Kategorie", "category_id": kat}) admin.post("/api/transactions", json={"type": "income", "date": "2026-04-03", "account_id": konto["id"], "amount_cents": 3000, "payee": "Einnahme ohne Kategorie"}) offen = admin.get("/api/transactions", params={"uncategorized": "true", "account_id": konto["id"]}).json() namen = sorted(t["payee"] for t in offen) assert namen == ["Einnahme ohne Kategorie", "Ohne Kategorie"] def test_offene_buchungen_lassen_uebertraege_aus(admin): """Ein Übertrag ordnet kein Geld ein, er verschiebt es – er gehört nicht auf die Liste.""" a = _konto(admin, name="Von") b = _konto(admin, name="Nach") admin.post("/api/transactions", json={"type": "transfer", "date": "2026-04-04", "account_id": a["id"], "to_account_id": b["id"], "amount_cents": 5000}) offen = admin.get("/api/transactions", params={"uncategorized": "true", "account_id": a["id"]}).json() assert offen == [] def test_verknuepfte_buchungen_sind_nicht_mehr_offen(admin): """Sie gehoeren zu einem Uebertrag und zaehlen in keiner Auswertung. Nach einer Kategorie zu fragen, verlangt Arbeit, die nichts bewirkt.""" import secrets a = admin.post("/api/accounts", json={ "name": f"Offen-A {secrets.token_hex(3)}", "type": "girokonto", "currency": "CHF", "opening_balance_cents": 0}).json() b = admin.post("/api/accounts", json={ "name": f"Offen-B {secrets.token_hex(3)}", "type": "girokonto", "currency": "CHF", "opening_balance_cents": 0}).json() betrag = 20000 + secrets.randbelow(900) raus = admin.post("/api/transactions", json={ "type": "expense", "date": "2026-08-21", "account_id": a["id"], "amount_cents": betrag, "payee": "Umbuchung"}).json() rein = admin.post("/api/transactions", json={ "type": "income", "date": "2026-08-21", "account_id": b["id"], "amount_cents": betrag, "payee": "Umbuchung"}).json() offen = {t["id"] for t in admin.get("/api/transactions", params={"uncategorized": True}).json()} assert raus["id"] in offen and rein["id"] in offen, "vorher beide offen" assert admin.post("/api/uebertraege/verbinden", json={"a_id": raus["id"], "b_id": rein["id"]}).status_code == 200 danach = {t["id"] for t in admin.get("/api/transactions", params={"uncategorized": True}).json()} assert raus["id"] not in danach and rein["id"] not in danach, \ "verknuepfte Buchungen brauchen keine Kategorie mehr" def test_gleich_aussehende_buchungen_werden_gemeldet_nicht_entschieden(admin): """Drei Aufrundungen ueber 1.00 am selben Tag sind echt, drei gleiche Einkaeufe koennen es auch sein – das weiss nur der Nutzer. Deshalb wird gemeldet, nicht entschieden, und die Kennung der Bank steht dabei.""" import secrets from app.database import SessionLocal from app.models import InboxItem, InboxSource, InboxStatus, User konto = admin.post("/api/accounts", json={ "name": f"Dublette {secrets.token_hex(3)}", "type": "girokonto", "currency": "CHF", "opening_balance_cents": 0}).json() text = f"Migrolino {secrets.token_hex(3)}" ids = [admin.post("/api/transactions", json={ "type": "expense", "date": "2026-08-25", "account_id": konto["id"], "amount_cents": 390, "payee": text}).json()["id"] for _ in range(3)] # Zwei davon kamen von der Bank, unter verschiedenen Kennungen. db = SessionLocal() try: besitzer = db.query(User).order_by(User.id).first() for i, txn_id in enumerate(ids[:2]): db.add(InboxItem(owner_id=besitzer.id, source=InboxSource.bank, status=InboxStatus.booked, account_id=konto["id"], date=__import__("datetime").date(2026, 8, 25), amount_cents=-390, currency="CHF", payee=text, external_id=f"acc-x:ref-{i}", transaction_id=txn_id)) db.commit() finally: db.close() gruppen = admin.get("/api/transactions/dubletten").json()["gruppen"] meine = next(g for g in gruppen if g["account_id"] == konto["id"]) assert len(meine["zeilen"]) == 3 assert meine["amount_cents"] == 390 kennungen = {z["kennung"] for z in meine["zeilen"]} assert kennungen == {"ref-0", "ref-1", None}, "die Kennung muss dabeistehen" assert {z["herkunft"] for z in meine["zeilen"]} == {"bank", "manuell"} # Und nichts wurde geloescht. assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == -1170