59 lines
3.0 KiB
Python
59 lines
3.0 KiB
Python
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)
|
|
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()]
|
|
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))
|
|
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
|