feat: Eine Kreditkarte ist eine Schuld – der Rahmen ist kein Guthaben

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>
This commit is contained in:
Scarriffle
2026-09-30 20:27:30 +02:00
co-authored by Claude Opus 5
parent 152483707e
commit 38697f828e
11 changed files with 320 additions and 5 deletions
+14
View File
@@ -313,6 +313,20 @@ class Account(Base):
color: Mapped[str] = mapped_column(String(9), nullable=False, default="#3f51b5")
opening_balance_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
opening_date: Mapped[date_t | None] = mapped_column(Date, nullable=True)
# Was die Karte ausgeben DARF – nicht, was auf ihr liegt.
#
# Eine Kreditkarte ist kein Guthaben. Abbezahlt steht sie auf null, jede
# Ausgabe drueckt sie ins Minus, die Rechnungszahlung holt sie zurueck.
# Der Rahmen ist eine Eigenschaft des Vertrags mit der Bank und hat im
# Saldo nichts zu suchen – wer ihn dort eintraegt, sieht Geld, das er
# nicht hat.
#
# Deshalb ein eigenes Feld: Die Zahl ist es wert, aufgehoben zu werden
# („noch 1.900 von 2.500 frei"), nur eben nicht als Vermoegen.
#
# Optional, und nur bei Kreditkarten sinnvoll. Ohne Eintrag aendert sich
# nichts.
credit_limit_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
# Wer hierher ueberweist, spart – und zwar gegen DIESE Kategorie.
#
# Sparen ist eine Entscheidung ueber ein KONTO, nicht ueber jede einzelne
+8 -2
View File
@@ -67,6 +67,7 @@ def to_out(db: Session, acc: Account, base: str, on: date,
color=acc.color,
opening_balance_cents=acc.opening_balance_cents,
opening_date=acc.opening_date,
credit_limit_cents=acc.credit_limit_cents,
is_archived=acc.is_archived,
position=acc.position,
savings_category_id=acc.savings_category_id,
@@ -162,6 +163,7 @@ def create_account(
color=payload.color,
opening_balance_cents=payload.opening_balance_cents,
opening_date=payload.opening_date,
credit_limit_cents=payload.credit_limit_cents,
savings_category_id=payload.savings_category_id,
position=(max_pos[0] + 1) if max_pos else 0,
)
@@ -196,8 +198,12 @@ def update_account(
_pruefe_sparziel(db, user, data["savings_category_id"])
for key, value in data.items():
# "savings_category_id" gehoert in die Ausnahme: Ohne sie liesse sich
# ein einmal gesetztes Sparziel nie wieder loesen.
if value is not None or key in ("opening_date", "savings_category_id"):
# ein einmal gesetztes Sparziel nie wieder loesen. Fuer den
# Kreditrahmen gilt dasselbe – wer ein Konto vom Typ Kreditkarte auf
# Girokonto umstellt, muss den Rahmen wieder loswerden koennen.
if value is not None or key in (
"opening_date", "savings_category_id", "credit_limit_cents"
):
setattr(acc, key, value)
if identifiers is not None:
_set_identifiers(acc, payload.identifiers)
+4
View File
@@ -162,6 +162,8 @@ class AccountCreate(BaseModel):
color: str = "#3f51b5"
opening_balance_cents: int = 0
opening_date: date_t | None = None
# Was die Karte ausgeben darf – kein Guthaben. Nur bei Kreditkarten sinnvoll.
credit_limit_cents: int | None = Field(default=None, ge=0)
# Ueberweisungen hierher zaehlen als Sparen gegen diese Ausgabe-Kategorie.
savings_category_id: int | None = None
identifiers: list[AccountIdentifierIn] = Field(default_factory=list)
@@ -186,6 +188,7 @@ class AccountUpdate(BaseModel):
color: str | None = None
opening_balance_cents: int | None = None
opening_date: date_t | None = None
credit_limit_cents: int | None = Field(default=None, ge=0)
is_archived: bool | None = None
position: int | None = None
savings_category_id: int | None = None
@@ -207,6 +210,7 @@ class AccountOut(BaseModel):
color: str
opening_balance_cents: int
opening_date: date_t | None
credit_limit_cents: int | None = None
is_archived: bool
position: int
savings_category_id: int | None = None