Add CSV importers, runway calculator, budget alerts, cross-hub coupling emitters, finhub CLI, and raas-mvp-packaging docs with full test coverage.
60 lines
No EOL
1.7 KiB
Python
60 lines
No EOL
1.7 KiB
Python
"""Budget and runway alert evaluation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from fin_hub.services.runway import RunwayResult
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BudgetAlert:
|
|
code: str
|
|
severity: str
|
|
summary: str
|
|
detail: dict
|
|
|
|
def as_dict(self) -> dict:
|
|
return {
|
|
"code": self.code,
|
|
"severity": self.severity,
|
|
"summary": self.summary,
|
|
"detail": self.detail,
|
|
}
|
|
|
|
|
|
def evaluate_budget_alerts(
|
|
*,
|
|
runway: RunwayResult,
|
|
allocated: float | None = None,
|
|
spent: float | None = None,
|
|
) -> list[BudgetAlert]:
|
|
alerts: list[BudgetAlert] = []
|
|
if runway.below_threshold and runway.monthly_burn > 0:
|
|
alerts.append(
|
|
BudgetAlert(
|
|
code="runway_below_threshold",
|
|
severity="high",
|
|
summary=(
|
|
f"Projected runway {runway.months_remaining:.1f} months "
|
|
f"is below threshold {runway.alert_threshold_months:.1f}"
|
|
),
|
|
detail=runway.as_dict(),
|
|
)
|
|
)
|
|
if allocated is not None and spent is not None and allocated > 0:
|
|
utilisation = spent / allocated
|
|
if utilisation >= 0.8:
|
|
alerts.append(
|
|
BudgetAlert(
|
|
code="budget_pressure",
|
|
severity="medium" if utilisation < 1.0 else "high",
|
|
summary=f"Budget utilisation at {utilisation * 100:.0f}%",
|
|
detail={
|
|
"allocated": allocated,
|
|
"spent": spent,
|
|
"utilisation": utilisation,
|
|
},
|
|
)
|
|
)
|
|
return alerts |