from datetime import date, timedelta from datetime import datetime, timezone from decimal import Decimal from sqlalchemy import delete, select from sqlalchemy.orm import Session from app.config import get_settings from app.engine.loans import loan_schedule from app.engine.projection import project from app.engine.scenario import (PlainModifier, PlainPlanned, PlainRecurring, build_cashflows) from app.models.tables import (Loan, PlannedItem, ProjectionPoint, ProjectionResult, RecurringItem, Scenario, ScenarioLoan, ScenarioModifier, ScenarioPlannedItem) from app.services.balances import total_balance def run_projection(session: Session, scenario: Scenario, horizon_days: int, start_date: date) -> ProjectionResult: end = start_date + timedelta(days=horizon_days) recurring = [] if scenario.include_recurring: recurring = [PlainRecurring(r.id, r.name, Decimal(r.amount), r.rhythm, r.due_day, r.start_date, r.end_date, r.category_id) for r in session.execute(select(RecurringItem)).scalars()] planned = [] if scenario.include_planned: planned = [PlainPlanned(p.name, Decimal(p.amount), p.due, p.category_id) for p in session.execute(select(PlannedItem)).scalars()] # Szenario-eigene Einmalzahlungen zaehlen IMMER fuer dieses Szenario - # include_planned steuert nur die globalen PlannedItems. planned += [PlainPlanned(p.name, Decimal(p.amount), p.due, None) for p in session.execute(select(ScenarioPlannedItem).where( ScenarioPlannedItem.scenario_id == scenario.id)).scalars()] loans = session.execute( select(Loan).join(ScenarioLoan, ScenarioLoan.loan_id == Loan.id) .where(ScenarioLoan.scenario_id == scenario.id)).scalars().all() schedules = [loan_schedule(Decimal(l.principal), Decimal(l.annual_rate_pct), l.term_months, l.payout_date, l.repayment_type) for l in loans] payouts = [(l.payout_date, Decimal(l.principal)) for l in loans] modifiers = [PlainModifier(m.target_type, m.target_id, m.kind, Decimal(m.value), m.end_date) for m in session.execute(select(ScenarioModifier).where( ScenarioModifier.scenario_id == scenario.id)).scalars()] flows = build_cashflows(recurring, planned, schedules, payouts, modifiers, start_date, end) proj = project(total_balance(session), start_date, flows, horizon_days, threshold=get_settings().warn_threshold) session.execute(delete(ProjectionPoint).where( ProjectionPoint.scenario_id == scenario.id)) for d, bal in proj.series: session.add(ProjectionPoint(scenario_id=scenario.id, day=d, balance=bal)) result = session.get(ProjectionResult, scenario.id) or ProjectionResult( scenario_id=scenario.id, computed_at=datetime.now(timezone.utc), low_point_date=proj.low_point[0], low_point_balance=proj.low_point[1]) result.computed_at = datetime.now(timezone.utc) result.low_point_date, result.low_point_balance = proj.low_point result.below_zero_date = proj.first_below_zero result.below_threshold_date = proj.first_below_threshold session.add(result) session.commit() return result