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' }} +