Files
teamwallet/docs/superpowers/specs/2026-08-03-theoretischer-kassenstand-design.md
Bastian Wagner 5f619d649c docs: add spec and plan for theoretical cash balance line
Recreated from the main checkout, where these were committed to local
master but not yet pushed and thus missing from this fresh worktree.
2026-08-03 10:31:53 +02:00

6.5 KiB

Theoretischer Kassenstand (Ist + offene Beiträge) im Kassenstand-Verlauf

Status: approved Datum: 2026-08-03

Kontext

Ergänzung zum bestehenden Kassenstand-Verlauf-Chart aus docs/superpowers/specs/2026-08-01-kasse-kpi-charts-design.md. Der Chart auf der Team-Übersicht (features/team/overview/overview.ts + .html) zeigt aktuell eine Linie „Kassenstand" über die letzten 12 Monate, gespeist aus GET /teams/:id/overview/stats bzw. teams.service.ts#getOverviewStats.

Auf derselben Übersicht existiert bereits eine zweite Kennzahl „Offene Beiträge" (aus teams.service.ts#getOverview, Zeile 46-53): -(Summe der balance aller aktiven Spieler). Sie zeigt nur den heutigen Wert, keinen Verlauf.

Ziel: eine zweite Linie im bestehenden Chart, die pro Monat den theoretischen Kassenstand zeigt — also „was wäre in der Kasse, wenn alle offenen Beiträge bereits bezahlt worden wären" —, um Trainer/ Kassenwarte auf einen Blick erkennen zu lassen, wie stark der Ist-Stand vom Soll-Stand abweicht.

Entscheidungen aus dem Brainstorming

  • Historie statt Snapshot: Die zweite Linie zeigt für jeden Monat die zu diesem Zeitpunkt tatsächlich offenen Beiträge, nicht den heutigen Wert konstant über alle 12 Punkte addiert.
  • Näherung wie beim bestehenden Kassenstand-Verlauf: Es wird mit der heutigen Menge aktiver Spieler gerechnet, kein historisches Tracking von Mitgliedschaft/Aktiv-Status. Das ist dieselbe Vereinfachung, die balanceHistory bereits für den Kassenstand selbst nutzt (siehe getOverviewStats-Kommentar zu team.balance als Anker).
  • Datenform: theoreticalBalance wird als zusätzliches Feld direkt in jeden bestehenden balanceHistory-Punkt eingebettet ({ month, balance, theoreticalBalance }), kein separates Array — additive, nicht-brechende Erweiterung der bestehenden Response.

Architektur / Komponenten

1. Backend: teams.service.ts#getOverviewStats

Neue private Hilfsberechnung, analog zur bestehenden Rückwärts-Rekonstruktion von balanceHistory (Zeile ~290-320), aber auf Spieler-Ebene statt Team-Ebene:

  • Datenquelle: team.players (bereits geladen über relations: ['players', 'players.transactions', 'transactions']), gefiltert auf player.active — dieselbe Teilmenge, die getOverview für die heutige „Offene Beiträge"-Kachel verwendet.
  • Für jeden aktiven Spieler: dessen transactions (bereits geladen), ausgenommen Zeilen mit note?.startsWith(DEACTIVATION_ADJUSTMENT_NOTE_PREFIX) (Import aus team-members.service.ts) — dieselbe Ausschlussregel wie in TeamMembersService.recomputeBalance, damit synthetische Ausgleichsbuchungen die Historie nicht verfälschen.
  • Vorzeichen je Buchung: type.id > 10 (Strafe/Umlage/Gebühr, IDs 11-13) mindert den Spieler-Saldo, alle anderen Typen (payment, credit) erhöhen ihn — identische Regel wie in TeamMembersService.recomputeBalance (Zeile ~127-133) und TransactionsService.reverse() (type.id > 10-Check).
  • Rekonstruktion: ausgehend von player.balance (aktueller, autoritativer Wert) rückwärts durch die nach Datum absteigend sortierten Buchungen laufen und pro Monat der letzten 12 Monate den rekonstruierten Saldo am Monatsende ermitteln — strukturell identisch zum bestehenden descendingMovements/futureSum-Muster für balanceHistory, nur pro Spieler statt einmal fürs Team.
  • Pro Monat: outstandingAtMonth = -Σ(rekonstruierter Saldo aktiver Spieler), theoreticalBalance = balanceHistory[monat].balance + outstandingAtMonth.
  • Rückgabeform ändert sich zu:
    balanceHistory: { month: string; balance: number; theoreticalBalance: number }[]
    
    monthlyFlow und topOutstanding bleiben unverändert.
  • Gating unverändert: Ist movements.length === 0 (keine Kassenbewegung je), bleibt balanceHistory: [] wie heute — ein Team mit ausschließlich unbezahlten Strafen, aber ganz ohne Zahlungsbewegung, zeigt weiterhin keinen Chart (Out of Scope, siehe unten).

2. Frontend: overview.ts / Chart-Konfiguration

  • models/team-stats.model.ts: BalanceHistoryPoint um theoreticalBalance: number erweitern.
  • balanceChartData (computed) bekommt eine zweite Dataset-Eintrag:
    • Label: „Theoretisch (inkl. offene Beiträge)"
    • data: points.map((p) => p.theoreticalBalance)
    • Gestrichelt (borderDash: [6, 4]), eigene Farbe #1d70b8 (Blauton, klar unterscheidbar vom Grün #4f8f46 der Ist-Linie), fill: false.
  • balanceChartOptions: plugins.legend.display von false auf true (bzw. position: 'bottom' wie beim Flow-Chart), da jetzt zwei Linien unterschieden werden müssen.
  • Keine Änderung an ChartCanvas (shared component) nötig — reine Config-/Daten-Änderung.

Fehlerbehandlung

Unverändert zum bestehenden Muster: Fehler beim Laden der Stats führen zum bestehenden stillen Empty-State der Chart-Karte. Kein neuer Fehlerfall durch diese Erweiterung.

Testing

  • Backend (teams.service.spec.ts, Erweiterung des bestehenden getOverviewStats-Testblocks):
    • Sanity-Check: balanceHistory.at(-1).theoreticalBalance === team.balance + aktuelle Summe offener Beiträge (heutiger Wert, wie von getOverview berechnet).
    • Historische Rekonstruktion: Testfall mit einer Strafe (fine) in einem früheren Monat, die erst im aktuellen Monat bezahlt wurde — theoreticalBalance im früheren Monat muss die damals offene Strafe enthalten, balance (Ist) nicht.
    • Deaktivierungs-Ausgleichsbuchungen werden aus der Rekonstruktion ausgeschlossen (Testfall mit einem zwischenzeitlich deaktivierten und wieder aktivierten Spieler).
    • Leerfall (movements.length === 0) liefert weiterhin balanceHistory: [].
  • Frontend (overview.spec.ts): Erweiterung des bestehenden Chart-Daten-Tests um Assertion, dass balanceChartData() zwei Datasets enthält und die zweite Serie aus theoreticalBalance gespeist wird.
  • Manuelle Verifikation: Team mit einer unbezahlten Strafe/Umlage lokal aufrufen, prüfen dass die theoretische Linie sichtbar über der Ist-Linie liegt und bei vollständiger Bezahlung beide Linien zusammenlaufen.

Out of Scope

  • Historisches Tracking von Mitgliedschaft/Aktiv-Status (Näherung mit heutiger aktiver Spieler-Menge, siehe oben).
  • Teams mit ausschließlich unbezahlten Strafen/Umlagen/Gebühren, aber ganz ohne Kassenbewegung — zeigen weiterhin keinen Chart (bestehende Einschränkung aus dem Basis-Feature, nicht neu eingeführt).
  • Zeitraum-Umschalter (weiterhin feste letzte 12 Monate, wie im Basis-Feature festgelegt).