"""Welche Buchung gehoert zu welchem Vertrag. Die Zuordnung ist ein ETIKETT: An der Buchung aendert sich nichts. Die Tests pruefen deshalb vor allem, was gleich bleiben MUSS – und wo das Programm lieber fragt, statt zu raten. """ import secrets from datetime import date, timedelta def _konto(admin, name=None, currency="EUR"): r = admin.post("/api/accounts", json={ "name": name or ("Konto " + secrets.token_hex(3)), "type": "girokonto", "currency": currency, "opening_balance_cents": 0}) assert r.status_code == 201, r.text return r.json() def _abo(admin, **kw): body = {"name": "Netflix " + secrets.token_hex(3), "amount_cents": 1990, "currency": "EUR", "interval_months": 1} body.update(kw) r = admin.post("/api/subscriptions", json=body) assert r.status_code == 201, r.text return r.json() def _buchung(admin, konto, betrag, datum, payee="", beschreibung=""): r = admin.post("/api/transactions", json={ "type": "expense", "date": datum, "account_id": konto["id"], "amount_cents": betrag, "payee": payee, "description": beschreibung}) assert r.status_code == 201, r.text return r.json() def _zuordnen(admin): r = admin.post("/api/subscriptions/zuordnen") assert r.status_code == 200, r.text return r.json() def _txn(admin, txn_id): for t in admin.get("/api/transactions").json(): if t["id"] == txn_id: return t raise AssertionError("Buchung verschwunden") def _aufraeumen(admin, abo=None, txns=()): for t in txns: admin.delete(f"/api/transactions/{t}") if abo: admin.delete(f"/api/subscriptions/{abo['id']}") # --- Der Normalfall ---------------------------------------------------------- def test_abbuchung_findet_ihren_vertrag(admin): konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="NETFLIX.COM 4711") try: assert _zuordnen(admin)["zugeordnet"] >= 1 verlauf = admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json() assert verlauf["anzahl"] == 1 assert verlauf["summe_cents"] == 1990 assert verlauf["buchungen"][0]["txn_id"] == b["id"] finally: _aufraeumen(admin, abo, [b["id"]]) def test_die_buchung_bleibt_unveraendert(admin): """Es ist ein Etikett. Betrag, Konto und Kategorie bleiben, wie sie sind.""" konto = _konto(admin) abo = _abo(admin, name="Spotify", amount_cents=1299, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1299, str(date.today()), payee="SPOTIFY AB") try: _zuordnen(admin) danach = _txn(admin, b["id"]) assert danach["amount_cents"] == 1299 assert danach["account_id"] == konto["id"] assert danach["date"] == b["date"] assert danach["type"] == "expense" finally: _aufraeumen(admin, abo, [b["id"]]) def test_faelligkeit_rueckt_nach_der_erkannten_abbuchung_weiter(admin): konto = _konto(admin) heute = date.today() abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(heute), interval_months=1) b = _buchung(admin, konto, 1990, str(heute), payee="NETFLIX") try: _zuordnen(admin) danach = admin.get(f"/api/subscriptions/{abo['id']}").json() assert danach["next_due"] is not None assert danach["next_due"] > str(heute), "Faelligkeit blieb stehen" finally: _aufraeumen(admin, abo, [b["id"]]) def test_jede_abbuchung_wird_einzeln_erkannt(admin): """Drei Monate nachgetragen ergeben drei Zeilen im Verlauf, nicht eine.""" konto = _konto(admin) heute = date.today() abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(heute), interval_months=1) tage = [heute - timedelta(days=62), heute - timedelta(days=31), heute] ids = [_buchung(admin, konto, 1990, str(t), payee="NETFLIX")["id"] for t in tage] try: _zuordnen(admin) verlauf = admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json() assert verlauf["anzahl"] == 3, verlauf assert verlauf["summe_cents"] == 3 * 1990 finally: _aufraeumen(admin, abo, ids) # --- Wo das Programm lieber fragt -------------------------------------------- def test_fremder_betrag_wird_nicht_zugeordnet(admin): """Lieber nichts sagen als daneben raten.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 7350, str(date.today()), payee="BAUMARKT") try: _zuordnen(admin) assert _txn(admin, b["id"]).get("subscription_id") in (None, 0) assert admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json()["anzahl"] == 0 finally: _aufraeumen(admin, abo, [b["id"]]) def test_richtiger_betrag_fremder_text_kommt_als_vorschlag(admin): """Auf dem Auszug steht oft der Abwickler, nicht die Marke. Fragen statt raten – aber die Frage muss gestellt werden.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="STRIPE PAYMENTS") try: antwort = _zuordnen(admin) assert _txn(admin, b["id"]).get("subscription_id") in (None, 0), \ "ungefragt zugeordnet" passend = [v for v in antwort["vorschlaege"] if v["txn_id"] == b["id"]] assert passend, antwort["vorschlaege"] assert passend[0]["subscription_id"] == abo["id"] finally: _aufraeumen(admin, abo, [b["id"]]) def test_buchung_auf_fremdem_konto_zaehlt_nicht(admin): """Der Vertrag sagt, ueber welches Konto er laeuft – das gilt.""" konto = _konto(admin) anderes = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, anderes, 1990, str(date.today()), payee="NETFLIX") try: _zuordnen(admin) assert _txn(admin, b["id"]).get("subscription_id") in (None, 0) finally: _aufraeumen(admin, abo, [b["id"]]) def test_gekuendigte_vertraege_ziehen_nichts_mehr_an(admin): konto = _konto(admin) abo = _abo(admin, name="Altes Abo", amount_cents=1990, account_id=konto["id"], next_due=str(date.today()), is_active=False) b = _buchung(admin, konto, 1990, str(date.today()), payee="ALTES ABO") try: _zuordnen(admin) assert _txn(admin, b["id"]).get("subscription_id") in (None, 0) finally: _aufraeumen(admin, abo, [b["id"]]) # --- Die Preiserhoehung ------------------------------------------------------ def test_preiserhoehung_wird_gemeldet_statt_verbucht(admin): """Der wertvollste Fall – und einer, den man nie automatisch nachziehen darf. Eine Preiserhoehung ist eine Nachricht an den Nutzer.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1790, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="NETFLIX.COM") try: antwort = _zuordnen(admin) meldungen = [p for p in antwort["preisaenderungen"] if p["txn_id"] == b["id"]] assert meldungen, antwort assert meldungen[0]["erwartet_cents"] == 1790 assert meldungen[0]["amount_cents"] == 1990 # Ungefragt geaendert hat sich nichts. assert admin.get(f"/api/subscriptions/{abo['id']}").json()["amount_cents"] == 1790 assert _txn(admin, b["id"]).get("subscription_id") in (None, 0) finally: _aufraeumen(admin, abo, [b["id"]]) def test_neuen_preis_uebernehmen_und_zuordnen(admin): """Der Weg, den der Nutzer danach geht.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1790, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="NETFLIX.COM") try: r = admin.post(f"/api/subscriptions/{abo['id']}/preis", params={"amount_cents": 1990}) assert r.status_code == 200, r.text assert r.json()["amount_cents"] == 1990 r = admin.post(f"/api/subscriptions/{abo['id']}/buchungen/{b['id']}") assert r.status_code == 200, r.text assert admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json()["anzahl"] == 1 finally: _aufraeumen(admin, abo, [b["id"]]) def test_ein_vielfaches_gilt_nicht_als_preisaenderung(admin): """Ein Abo, das sich verdreifacht, ist kein Abo mehr, sondern etwas anderes – sonst zieht jeder groessere Einkauf beim selben Haendler an.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 19900, str(date.today()), payee="NETFLIX.COM") try: antwort = _zuordnen(admin) assert not [p for p in antwort["preisaenderungen"] if p["txn_id"] == b["id"]] assert not [v for v in antwort["vorschlaege"] if v["txn_id"] == b["id"]] finally: _aufraeumen(admin, abo, [b["id"]]) # --- Das Etikett wieder abziehen --------------------------------------------- def test_zuordnung_laesst_sich_loesen_und_die_buchung_bleibt(admin): konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="NETFLIX") try: _zuordnen(admin) assert admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json()["anzahl"] == 1 r = admin.delete(f"/api/subscriptions/buchungen/{b['id']}") assert r.status_code == 204, r.text assert admin.get(f"/api/subscriptions/{abo['id']}/verlauf").json()["anzahl"] == 0 # Die Buchung selbst steht unveraendert da. assert _txn(admin, b["id"])["amount_cents"] == 1990 finally: _aufraeumen(admin, abo, [b["id"]]) def test_geloeschter_vertrag_nimmt_die_buchung_nicht_mit(admin): """Wer ein Abo kuendigt und loescht, will nicht seine Zahlungshistorie mitloeschen.""" konto = _konto(admin) abo = _abo(admin, name="Kurzlebig", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="KURZLEBIG") try: _zuordnen(admin) assert admin.delete(f"/api/subscriptions/{abo['id']}").status_code == 204 danach = _txn(admin, b["id"]) assert danach["amount_cents"] == 1990, "Buchung mitgeloescht" finally: admin.delete(f"/api/transactions/{b['id']}") def test_vorschlaege_veraendern_nichts(admin): """Eine Ansicht, die bloss hinsieht, darf nichts zuordnen.""" konto = _konto(admin) abo = _abo(admin, name="Netflix", amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee="NETFLIX") try: r = admin.get("/api/subscriptions/vorschlaege") assert r.status_code == 200, r.text assert r.json()["zugeordnet"] == 0 assert _txn(admin, b["id"]).get("subscription_id") in (None, 0) finally: _aufraeumen(admin, abo, [b["id"]]) def test_zuordnung_ueberlebt_sichern_und_einspielen(admin): """Ohne den Verweis in der Sicherung zeigte das Etikett danach auf einen FREMDEN Vertrag – die Nummern werden beim Einspielen neu vergeben.""" import json konto = _konto(admin) name = "Backup-Abo " + secrets.token_hex(3) abo = _abo(admin, name=name, amount_cents=1990, account_id=konto["id"], next_due=str(date.today())) b = _buchung(admin, konto, 1990, str(date.today()), payee=name) try: _zuordnen(admin) assert _txn(admin, b["id"])["subscription_id"] == abo["id"] roh = admin.get("/api/export/backup.json").content haufen = json.loads(roh) haufen = haufen.get("data", haufen) # Der Verweis muss ueberhaupt in der Sicherung stehen. gesichert = [t for t in haufen["transactions"] if t.get("subscription_id") is not None] assert gesichert, "kein einziges Etikett gesichert" r = admin.post("/api/backup/restore", files={"file": ("backup.json", roh, "application/json")}, data={"mode": "replace"}) assert r.status_code == 200, r.text # Nach dem Einspielen sind die Nummern andere – die Zuordnung muss # trotzdem auf DENSELBEN Vertrag zeigen. abos = admin.get("/api/subscriptions").json()["subscriptions"] wieder = next(s for s in abos if s["name"] == name) verlauf = admin.get(f"/api/subscriptions/{wieder['id']}/verlauf").json() assert verlauf["anzahl"] == 1, "Zuordnung nach dem Einspielen verloren" assert verlauf["summe_cents"] == 1990 finally: pass