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

112 lines
6.5 KiB
Markdown

# 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:
```ts
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).