Wer seinen Kreditrahmen festhalten wollte, hatte dafuer nur den
Anfangssaldo. Also standen dort 2.500, und Certain meldete ein Vermoegen,
das es nicht gibt: Das Geld ist nicht da, es darf nur ausgegeben werden.
Jetzt hat der Rahmen ein eigenes Feld – sichtbar nur bei Kreditkarten,
denn an einem Girokonto gaebe es nichts zu begrenzen. Die Kontokarte
zeigt daneben, was noch geht ("noch 1.754,50 von 2.500,00 frei") und
warnt, wenn der Rahmen ueberzogen ist.
Der Saldo selbst bleibt, was er ist: abbezahlt null, jede Ausgabe ins
Minus, die Rechnung holt ihn zurueck. Gerechnet werden musste dafuer
nichts neu – die Saldenrechnung war schon vorzeichenoffen.
Zwei Dinge fielen dabei auf und sind miterledigt:
- Die Summe "Gesamt" auf der Kontenseite zaehlte archivierte Konten mit,
das Backend nicht. Zwei Zahlen fuer dieselbe Sache.
- "Gesamt" wurde ungefaerbt ausgegeben. Solange nur Guthaben
zusammenkamen, fiel das nicht auf; mit einer Kartenschuld stuende ein
negatives Gesamt still und unauffaellig da.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
5.3 KiB
Python
135 lines
5.3 KiB
Python
"""Eine Kreditkarte ist eine Schuld, kein Guthaben.
|
||
|
||
Abbezahlt steht sie auf null. Jede Ausgabe drueckt sie ins Minus, die Zahlung
|
||
der Rechnung holt sie zurueck. Der Kreditrahmen ist, was sie ausgeben darf –
|
||
nicht, was auf ihr liegt.
|
||
|
||
Die Rechenkette war dafuer schon vorbereitet (balances.py rechnet
|
||
vorzeichenoffen). Diese Tests halten fest, dass sie es bleibt.
|
||
"""
|
||
|
||
import json
|
||
from datetime import date
|
||
|
||
|
||
def _karte(c, **kw):
|
||
body = {"name": "Visa", "type": "kreditkarte", "currency": "EUR",
|
||
"opening_balance_cents": 0}
|
||
body.update(kw)
|
||
r = c.post("/api/accounts", json=body)
|
||
assert r.status_code == 201, r.text
|
||
return r.json()
|
||
|
||
|
||
def test_kreditrahmen_wird_gespeichert_und_zurueckgegeben(admin):
|
||
karte = _karte(admin, credit_limit_cents=250000)
|
||
assert karte["credit_limit_cents"] == 250000
|
||
assert admin.get(f"/api/accounts/{karte['id']}").json()["credit_limit_cents"] == 250000
|
||
|
||
|
||
def test_kreditrahmen_laesst_sich_aendern_und_wieder_loeschen(admin):
|
||
"""Ohne die Loesch-Moeglichkeit haengt ein einmal gesetzter Rahmen fuer
|
||
immer am Konto – auch wenn daraus laengst ein Girokonto geworden ist."""
|
||
karte = _karte(admin, credit_limit_cents=250000)
|
||
|
||
r = admin.patch(f"/api/accounts/{karte['id']}", json={"credit_limit_cents": 300000})
|
||
assert r.json()["credit_limit_cents"] == 300000
|
||
|
||
r = admin.patch(f"/api/accounts/{karte['id']}", json={"credit_limit_cents": None})
|
||
assert r.json()["credit_limit_cents"] is None
|
||
|
||
|
||
def test_kontoname_aendern_laesst_den_rahmen_stehen(admin):
|
||
karte = _karte(admin, credit_limit_cents=250000)
|
||
r = admin.patch(f"/api/accounts/{karte['id']}", json={"name": "Visa Gold"})
|
||
assert r.json()["credit_limit_cents"] == 250000
|
||
|
||
|
||
def test_negativer_rahmen_wird_abgelehnt(admin):
|
||
r = admin.post("/api/accounts", json={
|
||
"name": "Unsinn", "type": "kreditkarte", "credit_limit_cents": -100})
|
||
assert r.status_code == 422, r.text
|
||
|
||
|
||
def test_karte_geht_ins_minus_und_die_rechnung_holt_sie_zurueck(admin):
|
||
"""Der ganze Lebenslauf einer Karte in einem Monat."""
|
||
giro = admin.post("/api/accounts", json={
|
||
"name": "Giro", "currency": "EUR", "opening_balance_cents": 100000}).json()
|
||
karte = _karte(admin, credit_limit_cents=250000)
|
||
heute = str(date.today())
|
||
|
||
def saldo(acc):
|
||
return admin.get(f"/api/accounts/{acc['id']}").json()["balance_cents"]
|
||
|
||
assert saldo(karte) == 0, "abbezahlt = null, nicht der Kreditrahmen"
|
||
|
||
for betrag in (600, 2400):
|
||
admin.post("/api/transactions", json={
|
||
"type": "expense", "date": heute, "account_id": karte["id"],
|
||
"amount_cents": betrag, "payee": "Laden"})
|
||
assert saldo(karte) == -3000, "Ausgaben druecken die Karte ins Minus"
|
||
|
||
# Die Rechnung: Geld geht vom Giro auf die Karte.
|
||
admin.post("/api/transactions", json={
|
||
"type": "transfer", "date": heute, "account_id": giro["id"],
|
||
"to_account_id": karte["id"], "amount_cents": 3000, "payee": "Kartenrechnung"})
|
||
|
||
assert saldo(karte) == 0, "bezahlt = wieder null"
|
||
assert saldo(giro) == 97000, "und das Geld ist wirklich vom Giro weg"
|
||
|
||
|
||
def test_vermoegen_zieht_die_kartenschuld_ab(admin):
|
||
"""Eine Schuld darf das Vermoegen nicht erhoehen – der Fehler, der
|
||
entsteht, wenn man den Kreditrahmen als Startsaldo eintraegt.
|
||
|
||
Gemessen wird die Veraenderung, nicht der absolute Wert: Konten aus
|
||
anderen Tests dieser Datei zaehlen sonst mit.
|
||
"""
|
||
from app.database import SessionLocal
|
||
from app.models import Account, User
|
||
from app.services import balances
|
||
|
||
def vermoegen() -> int:
|
||
db = SessionLocal()
|
||
try:
|
||
user = db.query(User).filter(User.username == "admin").one()
|
||
konten = db.query(Account).filter(Account.owner_id == user.id).all()
|
||
return balances.net_worth_cents(db, konten, "EUR", date.today())
|
||
finally:
|
||
db.close()
|
||
|
||
vorher = vermoegen()
|
||
|
||
admin.post("/api/accounts", json={
|
||
"name": "Giro Vermoegen", "currency": "EUR", "opening_balance_cents": 100000})
|
||
assert vermoegen() == vorher + 100000
|
||
|
||
karte = _karte(admin, name="Visa Vermoegen", credit_limit_cents=250000)
|
||
assert vermoegen() == vorher + 100000, "ein Kreditrahmen ist kein Vermoegen"
|
||
|
||
admin.post("/api/transactions", json={
|
||
"type": "expense", "date": str(date.today()), "account_id": karte["id"],
|
||
"amount_cents": 3000, "payee": "Laden"})
|
||
assert vermoegen() == vorher + 97000, "die Schuld wird abgezogen, nicht addiert"
|
||
|
||
|
||
def test_kreditrahmen_ueberlebt_das_backup(admin):
|
||
"""Ein Feld, das die Rueckspielung nicht ueberlebt, ist kein Feld."""
|
||
karte = _karte(admin, credit_limit_cents=250000)
|
||
|
||
daten = json.loads(admin.get("/api/export/backup.json").content)
|
||
gesichert = [a for a in daten["data"]["accounts"] if a["name"] == "Visa"]
|
||
assert gesichert and gesichert[0]["credit_limit_cents"] == 250000
|
||
|
||
admin.delete(f"/api/accounts/{karte['id']}")
|
||
r = admin.post(
|
||
"/api/backup/restore",
|
||
files={"file": ("backup.json", json.dumps(daten).encode("utf-8"),
|
||
"application/json")},
|
||
data={"mode": "replace"},
|
||
)
|
||
assert r.status_code == 200, r.text
|
||
|
||
zurueck = [a for a in admin.get("/api/accounts").json() if a["name"] == "Visa"]
|
||
assert zurueck and zurueck[0]["credit_limit_cents"] == 250000
|