From 57ab6dae4a3e8a42259023c047d381783193798e837e1099e64c0bc75a7270e8 Mon Sep 17 00:00:00 2001
From: wlfb
Date: Mon, 28 Sep 2026 19:01:21 +0200
Subject: [PATCH] =?UTF-8?q?feat:=20Szenario=20per=20GUI=20l=C3=B6schen,=20?=
=?UTF-8?q?Name=20System-Ist=20reserviert=20(A11-T1)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Opus 5.5 (1M context)
---
finance/app/models/views.py | 5 +++++
finance/app/routers/scenarios.py | 11 +++++++++++
finance/app/templates/szenarien.html | 3 +++
finance/tests/test_crud_api.py | 24 ++++++++++++++++++++++++
finance/tests/test_gui.py | 21 +++++++++++++++++++++
5 files changed, 64 insertions(+)
diff --git a/finance/app/models/views.py b/finance/app/models/views.py
index b8d7846..4c5854f 100644
--- a/finance/app/models/views.py
+++ b/finance/app/models/views.py
@@ -1,6 +1,11 @@
from sqlalchemy import text
from sqlalchemy.engine import Engine
+# Serienname der Ist-Kurve im Grafana-Panel „Szenario-Vergleich" (v_projection).
+# Für Szenarien reserviert (routers/scenarios.py lehnt ihn ab), damit kein
+# Szenario mit der Ist-Serie verschmelzen kann (Ausbaustufe 11).
+RESERVED_SCENARIO_NAME = "System-Ist"
+
# Per-account offset for the Konto-Saldo-Anker (Ausbaustufe 3 Task 1): the
# same math as app.services.balances.account_balance, expressed as a single
# constant "base" per account so it can be added to a plain (anchor-agnostic)
diff --git a/finance/app/routers/scenarios.py b/finance/app/routers/scenarios.py
index 70db717..325859c 100644
--- a/finance/app/routers/scenarios.py
+++ b/finance/app/routers/scenarios.py
@@ -13,6 +13,7 @@ from app.db import get_session
from app.models.tables import (Loan, ProjectionPoint, ProjectionResult,
Scenario, ScenarioLoan, ScenarioModifier,
ScenarioPlannedItem)
+from app.models.views import RESERVED_SCENARIO_NAME
from app.services.projection_service import run_projection
router = APIRouter(prefix="/api/scenarios", tags=["scenarios"],
@@ -85,6 +86,14 @@ def _get_scenario(session: Session, scenario_id: int) -> Scenario:
return scenario
+def _check_reserved_name(name: str | None) -> None:
+ """Der Serienname der Ist-Kurve (v_projection) ist für Szenarien tabu."""
+ if name is not None and (name.strip().casefold()
+ == RESERVED_SCENARIO_NAME.casefold()):
+ raise HTTPException(
+ 422, f"Der Name »{RESERVED_SCENARIO_NAME}« ist für die Ist-Kurve reserviert")
+
+
@router.get("", response_model=list[ScenarioOut])
def list_scenarios(session: Session = Depends(get_session)):
return [ScenarioOut.model_validate(s)
@@ -93,6 +102,7 @@ def list_scenarios(session: Session = Depends(get_session)):
@router.post("", response_model=ScenarioOut, status_code=201)
def create_scenario(data: ScenarioIn, session: Session = Depends(get_session)):
+ _check_reserved_name(data.name)
if session.execute(select(Scenario).where(Scenario.name == data.name)).scalar():
raise HTTPException(409, "Szenario existiert bereits")
scenario = Scenario(**data.model_dump())
@@ -107,6 +117,7 @@ def patch_scenario(scenario_id: int, data: ScenarioPatch,
session: Session = Depends(get_session)):
scenario = _get_scenario(session, scenario_id)
fields = data.model_dump(exclude_unset=True)
+ _check_reserved_name(fields.get("name"))
if "name" in fields and fields["name"] != scenario.name:
clash = session.execute(
select(Scenario).where(Scenario.name == fields["name"])).scalar()
diff --git a/finance/app/templates/szenarien.html b/finance/app/templates/szenarien.html
index 953b1db..cee2f10 100644
--- a/finance/app/templates/szenarien.html
+++ b/finance/app/templates/szenarien.html
@@ -13,6 +13,9 @@
· Wiederkehrende Posten: {{ 'ja' if sc.include_recurring else 'nein' }}
· Einmalposten: {{ 'ja' if sc.include_planned else 'nein' }}
+