From 404c1a5648f7dde6ec9d3a7d7b2b7a0e486859a9f7524e0d9052f3e991975ef5 Mon Sep 17 00:00:00 2001 From: wlfb Date: Mon, 28 Sep 2026 21:51:19 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Spec=20Ausbaustufe=2012=20(System-Pr?= =?UTF-8?q?=C3=A4fix,=20Doppelz=C3=A4hlungs-Warnung=20Kredit/Fixposten)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- .../specs/2026-09-28-ausbaustufe-12-design.md | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-28-ausbaustufe-12-design.md diff --git a/docs/superpowers/specs/2026-09-28-ausbaustufe-12-design.md b/docs/superpowers/specs/2026-09-28-ausbaustufe-12-design.md new file mode 100644 index 0000000..cff8ee6 --- /dev/null +++ b/docs/superpowers/specs/2026-09-28-ausbaustufe-12-design.md @@ -0,0 +1,249 @@ +# Design — Ausbaustufe 12: System-Präfix + Doppelzählungs-Warnung Kredit/Fixposten (v0.12.0) + +> Status: Design im Chat vom Nutzer freigegeben (2026-09-28), Spec zur +> Durchsicht. Anlass: (1) Eine echte Kategorie „Kreditraten" würde im +> Grafana-Panel „Planung nach Kategorie" mit der synthetischen +> Kreditraten-Serie verschmelzen. (2) Wird eine Kreditrate sowohl als +> wiederkehrender Posten als auch als Kredit gepflegt, zählt sie in Planung +> und Projektion doppelt — das passiert leicht versehentlich, ein Hilfetext +> allein schützt nicht. + +## Nutzerentscheidungen (2026-09-28) + +- **Reservierter Präfix „System-"** für alle vom Tool erzeugten Serien. + Kategorie- und Szenarionamen mit diesem Präfix werden abgelehnt. + Kreditraten-Serie heißt künftig **„System-Kreditraten"**; „System-Ist" + fällt unter dieselbe Regel. +- **„unkategorisiert" bleibt** unverändert (Verschmelzen mit einer echten + gleichnamigen Kategorie ist semantisch harmlos; Beschriftung bleibt + gleich zum Ist-Diagramm „Monat nach Kategorie"). +- **Aktiver Doppelzählungs-Check** statt Hilfevermerk: Speichern wird bei + Verdacht blockiert und ist nur nach **expliziter Bestätigung** in einem + Warnfenster möglich. +- **Toleranz** Betragsvergleich: **±3 %, mindestens 1,00 €**. +- **Dauerhafter Hinweis** auf der Planung-Seite an betroffenen Posten und + Krediten (nicht blockierend). +- **Version v0.12.0** (neues API-Verhalten). + +## Befund (Ist-Zustand, v0.11.0) + +- `RESERVED_SCENARIO_NAME = "System-Ist"` in `models/views.py`; + `routers/scenarios.py::_check_reserved_name` prüft nur diesen einen Namen. +- `services/planning_chart.py`: `KREDITRATEN = "Kreditraten"`. +- `routers/categories.py`: `create_category`/`patch_category` ohne + Namensreservierung. +- `routers/planning.py`: `create_recurring`/`patch_recurring`, + `create_loan`/`patch_loan` ohne Querprüfung. Schemas: `RecurringIn` + (Basis von `RecurringOut`), `RecurringPatch`, `LoanIn` (Basis von + `LoanOut`), `LoanPatch`. +- GUI-Formulare gegen diese Endpunkte (alle `hx-ext="json-form"`, + `planning.html`): Fixposten anlegen (`hx-post /api/recurring`), + bearbeiten (`hx-patch /api/recurring/{id}`), „Vorschlag übernehmen" + (`hx-post /api/recurring`), Kredit anlegen (`hx-post /api/loans`), + bearbeiten (`hx-patch /api/loans/{id}`). +- Fehleranzeige global in `base.html`: `htmx:responseError` → + `alert('Fehler: ' + responseText)`. +- Live-Bestand geprüft (2026-09-28): keine Kategorie/kein Szenario beginnt + mit „System-", keine Kategorie heißt „Kreditraten"/„unkategorisiert"; + 0 Kredite. +- Beratungsumgebung `/home/wlfb/fb`: Skill `finanz-api` dokumentiert + `POST /api/recurring` und `POST /api/loans`, Skill `finanzberatung` legt + Kredite per API an → vom neuen 409-Verhalten betroffen. + +## 1. Reservierter Präfix „System-" + +Neues Modul `app/reserved.py` (einzige Quelle): + +```python +SYSTEM_PREFIX = "System-" + +def is_reserved_name(name: str) -> bool: + """True, wenn name (strip + casefold) mit SYSTEM_PREFIX beginnt.""" + +def check_reserved_name(name: str | None) -> None: + """None -> nichts; reserviert -> HTTPException(422, ).""" +``` + +Meldung (422): **„Namen, die mit »System-« beginnen, sind für vom Tool +erzeugte Einträge reserviert"**. + +- `models/views.py`: `RESERVED_SCENARIO_NAME = f"{SYSTEM_PREFIX}Ist"` + (Wert unverändert „System-Ist", View-SQL unverändert). +- `services/planning_chart.py`: `KREDITRATEN = f"{SYSTEM_PREFIX}Kreditraten"`. +- `routers/scenarios.py`: `_check_reserved_name` entfällt, stattdessen + `check_reserved_name` in `create_scenario` und `patch_scenario` (gleiche + Stellen wie bisher). +- `routers/categories.py`: `check_reserved_name(data.name)` als erste + Zeile in `create_category` und `patch_category`. +- Grafana-Panel 5 zeigt ab dem nächsten Seitenaufruf „System-Kreditraten" + (Tabelle `planning_monthly` wird ohnehin komplett neu geschrieben — keine + Migration nötig). + +Tests: Kategorien und Szenarien je POST/PATCH mit „System-X", +„system-kreditraten", „ SYSTEM-Ist " → 422 mit Meldung; „Kreditraten", +„Systemkosten", „Ist-System" → erlaubt (201/200). Planning-Chart-Tests +nutzen die Konstante (Wert „System-Kreditraten"). + +## 2. Doppelzählungs-Erkennung (`app/services/double_count.py`) + +Reine Logik ohne HTTP-Abhängigkeit. + +```python +TOLERANZ_PROZENT = Decimal("3") +TOLERANZ_MIN = Decimal("1.00") +CODE = "doppelzaehlung_kredit" + +@dataclass(frozen=True) +class Konflikt: + posten_id: int | None # None = Posten noch nicht gespeichert + posten_name: str + posten_betrag: Decimal # negativ + kredit_id: int | None # None = Kredit noch nicht gespeichert + kredit_name: str + kredit_rate: Decimal # positiv, reguläre Monatsrate + +def regular_rate(principal, annual_rate_pct, term_months, repayment_type) -> Decimal | None +def rate_period(payout_date, term_months) -> tuple[date, date] +def is_conflict(posten, kredit) -> bool +def conflicts_for_recurring(session, posten, exclude_id=None) -> list[Konflikt] +def conflicts_for_loan(session, kredit, exclude_id=None) -> list[Konflikt] +def all_conflicts(session) -> list[Konflikt] +``` + +`posten`/`kredit` sind einfache Wertobjekte (Name, Betrag bzw. Kreditdaten, +Rhythmus, Start/Ende) — so lassen sich auch noch nicht gespeicherte bzw. +per PATCH veränderte Zustände prüfen. + +**Reguläre Monatsrate** (`regular_rate`, positiv, auf Cent gerundet): +- `annuity`: `engine/loans.py::annuity_payment(principal, rate, term)`. +- `bullet`: monatliche Zinsen `principal × rate / 100 / 12` (ROUND_HALF_UP). + Bei Zinssatz 0 → `None` (keine reguläre Rate → nie Konflikt). +- Schlusszahlung endfälliger Kredite wird nicht betrachtet. + +**Ratenzeitraum** (`rate_period`): erste Rate +`add_months(payout_date, 1)`, letzte Rate `add_months(payout_date, +term_months)` (identisch zu `loan_schedule`). + +**Konflikt** (`is_conflict`), wenn ALLE gelten: +1. Posten-Rhythmus `monthly` und Posten-Betrag `< 0`; +2. Kredit hat eine reguläre Rate `r` (nicht `None`); +3. `abs(abs(betrag) − r) ≤ max(TOLERANZ_MIN, r × TOLERANZ_PROZENT / 100)`; +4. Zeiträume überschneiden sich: `(start is None or start ≤ letzte_rate) + and (ende is None or ende ≥ erste_rate)`. + +Kein Namensvergleich (unzuverlässig). Bekannte Grenze: weicht der Posten +stärker als die Toleranz ab (z.B. Rate inkl. Versicherung), wird nichts +erkannt. Alle Beträge `Decimal`. + +Tests (Handrechnung, künstliche Werte): Annuität exakt / knapp innerhalb / +knapp außerhalb der 3 %-Grenze; Mindesttoleranz 1,00 € bei kleiner Rate; +endfällig (Zinsrate) inkl. Zinssatz 0 → kein Konflikt; Rhythmus +quarterly → kein Konflikt; positiver Betrag → kein Konflikt; Zeiträume +disjunkt (Posten endet vor erster Rate / startet nach letzter Rate) → +kein Konflikt; offene Start/Ende → Konflikt; `exclude_id` schließt den +bearbeiteten Datensatz selbst aus; `all_conflicts` findet mehrere Paare. + +## 3. API-Verhalten (`routers/planning.py`) + +- Neues Anfragefeld **`doppelzaehlung_bestaetigt: bool = False`** — nur in + den Request-Modellen (`RecurringCreate(RecurringIn)`, `RecurringPatch`, + `LoanCreate(LoanIn)`, `LoanPatch`), **nicht** in `RecurringOut`/`LoanOut`, + nicht persistiert, beim Anlegen der ORM-Objekte aus `model_dump` + ausgeschlossen. +- **POST** `/api/recurring` bzw. `/api/loans`: vor dem Speichern + `conflicts_for_recurring`/`conflicts_for_loan` für den neuen Datensatz; + Treffer und Flag nicht gesetzt → **409** (nichts gespeichert). +- **PATCH** `/api/recurring/{id}` bzw. `/api/loans/{id}`: Konflikte vor und + nach Anwendung der Änderung ermitteln (bezogen auf diesen Datensatz); + 409 nur, wenn nach der Änderung ein Konflikt-Paar existiert, das vorher + nicht existierte (ein bereits bestätigter Konflikt fragt bei reiner + Namensänderung nicht erneut). Bei 409 `session.rollback()`. +- **409-Antwort** (`HTTPException(409, detail={...})`): + ```json + {"detail": {"code": "doppelzaehlung_kredit", + "meldung": "", + "konflikte": [{"posten": "...", "posten_betrag": "-312.50", + "kredit": "...", "kredit_rate": "312.50"}]}} + ``` + Beträge als Strings mit Punkt-Dezimal (API-Konvention). +- Flag `true` → Speichern trotz Konflikt (201/200 wie bisher). +- Andere Endpunkte unverändert (Einmalposten, Szenario-Zuordnung). + +Tests: je Endpunkt (POST/PATCH × Posten/Kredit) Konflikt → 409 mit +`code`, `konflikte` und nichts gespeichert/geändert; mit Flag → 201/200 und +gespeichert; ohne Konflikt → unverändert 201/200; PATCH einer reinen +Namensänderung bei bestehendem Konflikt → 200 ohne Flag; `RecurringOut`/ +`LoanOut` enthalten kein `doppelzaehlung_bestaetigt`; bestehende Tests +bleiben grün. + +## 4. GUI: Warnfenster (`base.html`) + +- Ein `` im Basis-Layout mit Titel + „Mögliche Doppelzählung", Erklärtext (`meldung`), Liste der Konflikte + („Posten „X" (−312,50 €) ↔ Kredit „Y" (Rate 312,50 €)", Beträge deutsch + formatiert) und Warnsatz („Beide zusammen zählen die Rate doppelt — + Planung und Szenarien wären zu pessimistisch."). Buttons: + **„Abbrechen"** (`autofocus`, schließt ohne Speichern) und + **„Trotzdem speichern"**. +- `htmx:responseError`-Handler: Status 409 und JSON-`detail.code === + "doppelzaehlung_kredit"` → Dialog füllen + `showModal()`, **kein** + `alert`. Alle anderen Fehler wie bisher per `alert`. +- „Trotzdem speichern": setzt am auslösenden Formular + `data-doppelzaehlung-bestaetigt="1"` und löst es erneut aus + (`htmx.trigger(form, 'submit')`); die `json-form`-Extension ergänzt bei + gesetztem Attribut `doppelzaehlung_bestaetigt: true`; nach dem Request + (`htmx:afterRequest`) wird das Attribut wieder entfernt. +- Gilt automatisch für alle fünf Formulare (Anlegen/Bearbeiten Posten, + Vorschlag übernehmen, Anlegen/Bearbeiten Kredit). +- UX-Regel: Formular bleibt sichtbar; Abbrechen lässt Eingaben stehen. + +## 5. Dauerhafter Hinweis (Planung-Seite) + +- `routers/gui.py::planung_page` ermittelt `all_conflicts(session)` und + übergibt je Posten-ID bzw. Kredit-ID die Gegenseite(n). +- `planning.html`: in der Namens-Zelle betroffener Posten/Kredite ein + `⚠ zählt evtl. doppelt mit Kredit „Y"` + bzw. „… mit Posten „X"". Die Namens-Zelle bekommt `data-sort="{{ name }}"`, + damit der Hinweis die Sortierung nicht beeinflusst. +- CSS `.warn-hint` dezent (kleine Schrift, Warnfarbe), nicht blockierend. + +Tests: GUI `/planung` mit einem Konflikt-Paar zeigt den Hinweis an Posten +und Kredit; ohne Konflikt kein Hinweis; `data-sort` der Namenszellen +enthält nur den Namen. + +## 6. Doku / Beratungsumgebung / Release + +- `hilfe.html`: kurzer Absatz „Reserviert: Namen mit »System-«" und + „Doppelzählungs-Warnung Kredit/Fixposten" (was geprüft wird, Toleranz, + Bestätigung, Hinweis in der Planung). +- `docs/ARCHITEKTUR.md`: `reserved.py`, `double_count.py` (Diagrammnamen + müssen im Code existieren). +- **`/home/wlfb/fb/.claude/skills/finanz-api/SKILL.md`** (eigenes Repo, + eigener Commit): Abschnitte „Wiederkehrende Posten" und „Kredite" um die + 409-Antwort `doppelzaehlung_kredit` und das Feld + `doppelzaehlung_bestaetigt` ergänzen, mit der Regel: **Flag nur nach + ausdrücklicher Rückfrage beim Nutzer setzen**; Reservierung „System-" für + Kategorien/Szenarien erwähnen. +- `finance/VERSION` 0.11.0 → **0.12.0**; Backup, Redeploy, Live-Smoke + (Wegwerf-Posten/-Kredit mit Konflikt → 409, mit Flag → 201, wieder + löschen; Kategorie „System-Test" → 422; Panel 5 zeigt + „System-Kreditraten" nur wenn Kredite existieren; Bestandszahlen + unverändert). + +## Abgrenzung + +- Keine Prüfung von Einmalposten (z.B. Schlusszahlung endfällig). +- Keine Prüfung bei Szenario-Kredit-Zuordnung. +- Kein Namensabgleich Posten↔Kredit. +- Keine Umbenennung von „unkategorisiert". +- Keine Migration (keine Schemaänderung). + +## Rahmenregeln + +- DATENSCHUTZ: Tests nur mit künstlichen Werten; keine Live-Beträge/-Namen + in Commits, Ledger, Chat. +- Decimal statt float für alle Beträge (auch Toleranzrechnung). +- Deutsch für GUI-Texte, Meldungen, Commits; Datum TT.MM.JJJJ. +- UX-Regel: Bedienelemente sichtbar; Warnung per Dialog mit expliziter + Bestätigung, sonst bestehender Fehler-Alert. +- Fable-Testagent-Gate je Plan-Task.