Files
certain/backend/tests/test_abos.py
T
ScarriffleandClaude Opus 5 0a790fd45a feat: Nur die Waehrungen zeigen, die man braucht – und auf dem Telefon lesbar
Drei Sachen, die beim Benutzen aufgefallen sind.

ANGEBOTENE WAEHRUNGEN. In den Einstellungen steht jetzt, welche Waehrungen in
den Auswahlfeldern erscheinen (Vorgabe EUR, CHF, USD). Dass dreissig
unterstuetzt werden, ist gut; dreissig Zeilen in einem Feld zu sehen, wenn man
drei davon braucht, ist es nicht.

Die Einstellung beschraenkt nur die ANZEIGE. Gueltig bleibt alles mit
EZB-Kurs, und die gerade gesetzte Waehrung steht IMMER im Feld – sonst stuende
ein Konto in Dollar vor einem Feld ohne Dollar und wechselte beim naechsten
Speichern stillschweigend die Waehrung. Der Saldo waere danach still falsch.
Die Hauptwaehrung laesst sich nicht abwaehlen.

ZEICHEN STATT KUERZEL, wo es hilft: "€ EUR", "$ USD", "£ GBP". CHF steht
bewusst ohne: "Fr." ist weniger gelaeufig als CHF selbst. Mehrfach belegte
Zeichen bleiben ebenfalls draussen – "US$" von "CA$" zu unterscheiden ist
Raetselraten.

AUF DEM TELEFON. Die Vertragszeile quetschte vier Spalten in 375 Pixel; vom
Namen blieb "Sal..." uebrig und von der Kategorie ein Punkt. Sie ist jetzt ein
Gitter: Unter 620 Pixel rutscht der Betrag unter den Namen, die Knoepfe
bleiben rechts. Der Name bekommt die volle Breite – auf dem Telefon ist er das
Einzige, woran man die Zeile erkennt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:16:45 +02:00

181 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Abos & Vertraege: verzeichnen und zusammenrechnen.
Der Nutzen dieser Stufe ist eine einzige Zahl – was die Vertraege zusammen im
Monat kosten. Alles andere haengt daran, dass sie stimmt: auch ueber
Waehrungen und ueber verschiedene Intervalle hinweg.
"""
import secrets
from datetime import date, timedelta
from conftest import login_ready, new_user
def _abo(admin, **kw):
body = {"name": "Netflix " + secrets.token_hex(3), "amount_cents": 1790,
"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 _liste(admin):
r = admin.get("/api/subscriptions")
assert r.status_code == 200, r.text
return r.json()
def _kategorie(admin, name):
for g in admin.get("/api/categories").json():
for c in g["categories"]:
if c["name"] == name:
return c["id"]
raise AssertionError(f"Kategorie '{name}' fehlt")
def test_monatswert_wird_aus_dem_intervall_gerechnet(admin):
"""Nicht gespeichert, sondern gerechnet – sonst veraltet er beim naechsten
Preisaufschlag stillschweigend."""
jahr = _abo(admin, amount_cents=40000, interval_months=12)
assert jahr["monatlich_cents"] == 3333
quartal = _abo(admin, amount_cents=3000, interval_months=3)
assert quartal["monatlich_cents"] == 1000
monat = _abo(admin, amount_cents=1790, interval_months=1)
assert monat["monatlich_cents"] == 1790
def test_preisaenderung_zieht_den_monatswert_mit(admin):
abo = _abo(admin, amount_cents=1790, interval_months=1)
r = admin.patch(f"/api/subscriptions/{abo['id']}", json={"amount_cents": 1990})
assert r.status_code == 200, r.text
assert r.json()["monatlich_cents"] == 1990
def test_summe_zaehlt_gekuendigte_nicht_mit(admin):
vorher = _liste(admin)["summary"]["monatlich_cents"]
abo = _abo(admin, amount_cents=2500, interval_months=1, currency="EUR")
assert _liste(admin)["summary"]["monatlich_cents"] == vorher + 2500
admin.patch(f"/api/subscriptions/{abo['id']}", json={"is_active": False})
assert _liste(admin)["summary"]["monatlich_cents"] == vorher
def test_fremde_waehrung_wird_umgerechnet(admin):
"""Ein deutscher Handyvertrag laeuft in Euro, auch wenn das Konto Franken
fuehrt. Ohne Umrechnung liessen sich die Zeilen nicht addieren."""
from app.database import SessionLocal
from app.models import ExchangeRate
with SessionLocal() as s:
if s.get(ExchangeRate, (date.today(), "CHF")) is None:
s.add(ExchangeRate(date=date.today(), currency="CHF",
rate_per_eur=0.95, source="test"))
s.commit()
abo = _abo(admin, amount_cents=1000, interval_months=1, currency="CHF")
# Hauptwaehrung ist EUR: 10.00 CHF bei 0.95 CHF je EUR sind 10.53 EUR.
assert abo["monatlich_base_cents"] == 1053
assert abo["monatlich_cents"] == 1000, "die eigene Waehrung bleibt unangetastet"
def test_waehrung_ohne_kurs_wird_gemeldet_statt_verschluckt(admin):
"""Lieber sagen, dass eine Zeile fehlt, als die Summe zu klein anzuzeigen."""
abo = _abo(admin, amount_cents=999, interval_months=1, currency="ZAR")
summary = _liste(admin)["summary"]
assert "ZAR" in summary["ohne_kurs"], summary
treffer = next(z for z in _liste(admin)["subscriptions"] if z["id"] == abo["id"])
assert treffer["monatlich_base_cents"] is None
def test_dollar_ist_jetzt_erlaubt(admin):
"""Manche Abos werden in Dollar abgerechnet. Frueher lehnte das Formular
alles ausser EUR und CHF ab."""
abo = _abo(admin, currency="USD", amount_cents=1200)
assert abo["currency"] == "USD"
def test_erfundene_waehrung_wird_abgelehnt(admin):
"""Ohne Kurs liesse sich der Betrag nie umrechnen – er stuende in keiner
Summe. Das faellt spaeter auf und ist dann schwer zu erklaeren."""
r = admin.post("/api/subscriptions", json={
"name": "Fantasie", "amount_cents": 100, "currency": "XYZ",
"interval_months": 1})
assert r.status_code == 422, r.text
def test_krummes_intervall_wird_abgelehnt(admin):
r = admin.post("/api/subscriptions", json={
"name": "Krumm", "amount_cents": 100, "currency": "EUR",
"interval_months": 37})
assert r.status_code == 422, r.text
def test_kategorie_muss_eine_ausgabe_sein(admin):
gehalt = _kategorie(admin, "Gehalt")
r = admin.post("/api/subscriptions", json={
"name": "Falsch", "amount_cents": 100, "currency": "EUR",
"interval_months": 1, "category_id": gehalt})
assert r.status_code == 400, r.text
assert "Ausgabe" in r.json()["detail"]
def test_erkennungstext_wird_aus_dem_namen_vorbelegt(admin):
abo = _abo(admin, name="Spotify Familie")
assert abo["payee_pattern"] == "Spotify Familie"
def test_faelligkeit_zaehlt_die_tage(admin):
abo = _abo(admin, next_due=str(date.today() + timedelta(days=9)))
assert abo["tage_bis_faellig"] == 9
ueberfaellig = _abo(admin, next_due=str(date.today() - timedelta(days=2)))
assert ueberfaellig["tage_bis_faellig"] == -2
def test_sparkonto_laesst_sich_wieder_loesen(admin):
"""Sonst wird man ein einmal gesetztes Pocket nie wieder los."""
konto = admin.post("/api/accounts", json={
"name": "Pocket " + secrets.token_hex(3), "type": "sparkonto",
"currency": "EUR", "opening_balance_cents": 0}).json()
abo = _abo(admin, interval_months=12, amount_cents=40000,
savings_account_id=konto["id"])
assert abo["savings_account_id"] == konto["id"]
r = admin.patch(f"/api/subscriptions/{abo['id']}", json={"savings_account_id": None})
assert r.status_code == 200, r.text
assert r.json()["savings_account_id"] is None
def test_fremdes_konto_wird_nicht_angenommen(admin):
"""Sonst schriebe ein Vertrag seine Zahlungen spaeter in ein Konto, das
einem anderen gehoert."""
fremd = admin.post("/api/accounts", json={
"name": "Fremd " + secrets.token_hex(3), "type": "girokonto",
"currency": "EUR", "opening_balance_cents": 0}).json()
username, password = new_user(admin)
anderer, _ = login_ready(username, password)
r = anderer.post("/api/subscriptions", json={
"name": "Fremdzugriff", "amount_cents": 100, "currency": "EUR",
"interval_months": 1, "account_id": fremd["id"]})
assert r.status_code == 404, r.text
def test_angebotene_waehrungen_sind_eine_einstellung(admin):
"""Die Liste beschraenkt nur die ANZEIGE. Gueltig bleibt alles mit Kurs –
sonst verloere ein Konto seine Waehrung, weil sie gerade nicht angehakt
ist."""
rows = {r["key"]: r["value"] for r in admin.get("/api/settings").json()}
assert rows["active_currencies"] == "EUR,CHF,USD", "Vorgabe passt nicht"
r = admin.put("/api/settings/active_currencies", params={"value": "EUR,CHF"})
assert r.status_code == 200, r.text
try:
# Trotz Abwahl weiter anlegbar: Die Einstellung ist keine Sperre.
abo = _abo(admin, currency="USD", amount_cents=500)
assert abo["currency"] == "USD"
finally:
admin.put("/api/settings/active_currencies", params={"value": "EUR,CHF,USD"})