from datetime import date from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, ConfigDict from sqlalchemy import select from sqlalchemy.orm import Session from app.auth import require_auth from app.config import get_settings from app.db import get_session from app.models.tables import (Loan, ProjectionPoint, Scenario, ScenarioLoan, ScenarioModifier) from app.services.projection_service import run_projection router = APIRouter(prefix="/api/scenarios", tags=["scenarios"], dependencies=[Depends(require_auth)]) class ScenarioIn(BaseModel): name: str description: str = "" include_recurring: bool = True include_planned: bool = True class ScenarioOut(ScenarioIn): model_config = ConfigDict(from_attributes=True) id: int class ScenarioPatch(BaseModel): name: str | None = None description: str | None = None include_recurring: bool | None = None include_planned: bool | None = None class ModifierIn(BaseModel): target_type: str target_id: int kind: str value: Decimal = Decimal("0") class ModifierOut(ModifierIn): model_config = ConfigDict(from_attributes=True) id: int scenario_id: int class SeriesPointOut(BaseModel): day: date balance: Decimal class ProjectionOut(BaseModel): low_point_date: date low_point_balance: Decimal below_zero_date: date | None below_threshold_date: date | None series: list[SeriesPointOut] def _get_scenario(session: Session, scenario_id: int) -> Scenario: scenario = session.get(Scenario, scenario_id) if scenario is None: raise HTTPException(404, "Szenario nicht gefunden") return scenario @router.get("", response_model=list[ScenarioOut]) def list_scenarios(session: Session = Depends(get_session)): return [ScenarioOut.model_validate(s) for s in session.execute(select(Scenario)).scalars()] @router.post("", response_model=ScenarioOut, status_code=201) def create_scenario(data: ScenarioIn, session: Session = Depends(get_session)): if session.execute(select(Scenario).where(Scenario.name == data.name)).scalar(): raise HTTPException(409, "Szenario existiert bereits") scenario = Scenario(**data.model_dump()) session.add(scenario) session.commit() session.refresh(scenario) return ScenarioOut.model_validate(scenario) @router.patch("/{scenario_id}", response_model=ScenarioOut) def patch_scenario(scenario_id: int, data: ScenarioPatch, session: Session = Depends(get_session)): scenario = _get_scenario(session, scenario_id) for key, value in data.model_dump(exclude_unset=True).items(): setattr(scenario, key, value) session.commit() session.refresh(scenario) return ScenarioOut.model_validate(scenario) @router.delete("/{scenario_id}", status_code=204) def delete_scenario(scenario_id: int, session: Session = Depends(get_session)): scenario = _get_scenario(session, scenario_id) session.delete(scenario) session.commit() @router.post("/{scenario_id}/loans/{loan_id}", status_code=204) def add_scenario_loan(scenario_id: int, loan_id: int, session: Session = Depends(get_session)): _get_scenario(session, scenario_id) if session.get(Loan, loan_id) is None: raise HTTPException(404, "Kredit nicht gefunden") if session.get(ScenarioLoan, (scenario_id, loan_id)) is None: session.add(ScenarioLoan(scenario_id=scenario_id, loan_id=loan_id)) session.commit() @router.delete("/{scenario_id}/loans/{loan_id}", status_code=204) def remove_scenario_loan(scenario_id: int, loan_id: int, session: Session = Depends(get_session)): _get_scenario(session, scenario_id) link = session.get(ScenarioLoan, (scenario_id, loan_id)) if link is not None: session.delete(link) session.commit() @router.post("/{scenario_id}/modifiers", response_model=ModifierOut, status_code=201) def add_modifier(scenario_id: int, data: ModifierIn, session: Session = Depends(get_session)): _get_scenario(session, scenario_id) modifier = ScenarioModifier(scenario_id=scenario_id, **data.model_dump()) session.add(modifier) session.commit() session.refresh(modifier) return ModifierOut.model_validate(modifier) @router.delete("/{scenario_id}/modifiers/{mod_id}", status_code=204) def delete_modifier(scenario_id: int, mod_id: int, session: Session = Depends(get_session)): _get_scenario(session, scenario_id) modifier = session.get(ScenarioModifier, mod_id) if modifier is None or modifier.scenario_id != scenario_id: raise HTTPException(404, "Modifikator nicht gefunden") session.delete(modifier) session.commit() @router.post("/{scenario_id}/project", response_model=ProjectionOut) def project_scenario(scenario_id: int, horizon_days: int | None = None, start_date: date | None = None, session: Session = Depends(get_session)): scenario = _get_scenario(session, scenario_id) settings = get_settings() resolved_horizon = horizon_days if horizon_days is not None else settings.horizon_days resolved_start = start_date if start_date is not None else date.today() result = run_projection(session, scenario, resolved_horizon, resolved_start) points = session.execute( select(ProjectionPoint).where(ProjectionPoint.scenario_id == scenario_id) .order_by(ProjectionPoint.day) ).scalars().all() return ProjectionOut( low_point_date=result.low_point_date, low_point_balance=result.low_point_balance, below_zero_date=result.below_zero_date, below_threshold_date=result.below_threshold_date, series=[SeriesPointOut(day=p.day, balance=p.balance) for p in points], )