Coverage for backend/django/Economics/results/services/financial_metrics/metric_catalog.py: 95%
71 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1"""Central financial metric catalog shared by results and comparisons."""
3from __future__ import annotations
5from enum import StrEnum
7from pydantic import BaseModel, ConfigDict
9from Economics.shared.choices import ResultLineKind
12class FinancialMetricKey(StrEnum):
13 CAPEX = "capex"
14 PURCHASE_BASIS_EQUIPMENT = "purchase_basis_equipment"
15 INSTALLED_BASIS_EQUIPMENT = "installed_basis_equipment"
16 CONTINGENCY = "contingency"
17 ELECTRICAL_UPGRADE = "electrical_upgrade"
18 PEAK_DEMAND = "peak_demand"
19 ANNUAL_DEPRECIATION = "annual_depreciation"
20 DEPRECIATION_TAX_SHIELD = "depreciation_tax_shield"
21 ANNUAL_OPEX = "annual_opex"
22 ANNUAL_REVENUE = "annual_revenue"
23 ANNUAL_PROFIT = "annual_profit"
24 ANNUAL_SAVINGS = "annual_savings"
25 ANNUAL_NET_BENEFIT = "annual_net_benefit"
26 AFTER_TAX_ANNUAL_CASH_FLOW = "after_tax_annual_cash_flow"
27 INCREMENTAL_CAPEX = "incremental_capex"
28 NPV = "npv"
29 SIMPLE_PAYBACK_YEARS = "simple_payback_years"
30 ROI_PERCENT = "roi_percent"
33class MetricFormat(StrEnum):
34 MONEY = "money"
35 MONEY_PER_YEAR = "money_per_year"
36 NUMBER = "number"
37 PERCENT = "percent"
38 YEARS = "years"
41class MetricComparisonDirection(StrEnum):
42 """Business meaning for ranking metric values inside study comparisons."""
44 HIGHER_IS_BETTER = "higher_is_better"
45 LOWER_IS_BETTER = "lower_is_better"
46 NOT_RANKED = "not_ranked"
49class FinancialMetricSpec(BaseModel):
50 """Display and calculation metadata for one financial metric."""
52 model_config = ConfigDict(frozen=True)
54 key: FinancialMetricKey
55 row_key: str
56 label: str
57 sort_order: int
58 line_kind: str
59 format: MetricFormat
60 baseline_dependent: bool = False
61 key_financial_metric: bool = False
62 key_metric_sort_order: int | None = None
63 default_comparison_metric: bool = False
64 maximum_fraction_digits: int = 0
65 comparison_direction: MetricComparisonDirection = (
66 MetricComparisonDirection.NOT_RANKED
67 )
70def _spec(
71 key: FinancialMetricKey,
72 *,
73 label: str,
74 sort_order: int,
75 line_kind: str,
76 format: MetricFormat,
77 baseline_dependent: bool = False,
78 key_financial_metric: bool = False,
79 key_metric_sort_order: int | None = None,
80 default_comparison_metric: bool = False,
81 maximum_fraction_digits: int = 0,
82 comparison_direction: MetricComparisonDirection = (
83 MetricComparisonDirection.NOT_RANKED
84 ),
85) -> FinancialMetricSpec:
86 """Create a catalog row and derive the persistent result row key from the enum key."""
87 return FinancialMetricSpec(
88 key=key,
89 row_key=f"metric.{key.value}",
90 label=label,
91 sort_order=sort_order,
92 line_kind=line_kind,
93 format=format,
94 baseline_dependent=baseline_dependent,
95 key_financial_metric=key_financial_metric,
96 key_metric_sort_order=key_metric_sort_order,
97 default_comparison_metric=default_comparison_metric,
98 maximum_fraction_digits=maximum_fraction_digits,
99 comparison_direction=comparison_direction,
100 )
103FINANCIAL_METRICS: tuple[FinancialMetricSpec, ...] = (
104 _spec(
105 FinancialMetricKey.CAPEX,
106 label="Total capital cost",
107 sort_order=0,
108 line_kind=ResultLineKind.CAPITAL,
109 format=MetricFormat.MONEY,
110 key_financial_metric=True,
111 key_metric_sort_order=60,
112 default_comparison_metric=True,
113 comparison_direction=MetricComparisonDirection.LOWER_IS_BETTER,
114 ),
115 _spec(
116 FinancialMetricKey.PURCHASE_BASIS_EQUIPMENT,
117 label="Purchased basis equipment",
118 sort_order=1,
119 line_kind=ResultLineKind.CAPITAL,
120 format=MetricFormat.MONEY,
121 ),
122 _spec(
123 FinancialMetricKey.INSTALLED_BASIS_EQUIPMENT,
124 label="Installed basis equipment",
125 sort_order=2,
126 line_kind=ResultLineKind.CAPITAL,
127 format=MetricFormat.MONEY,
128 ),
129 _spec(
130 FinancialMetricKey.CONTINGENCY,
131 label="Contingency",
132 sort_order=3,
133 line_kind=ResultLineKind.CAPITAL,
134 format=MetricFormat.MONEY,
135 ),
136 _spec(
137 FinancialMetricKey.ELECTRICAL_UPGRADE,
138 label="Electrical upgrade",
139 sort_order=4,
140 line_kind=ResultLineKind.CAPITAL,
141 format=MetricFormat.MONEY,
142 ),
143 _spec(
144 FinancialMetricKey.PEAK_DEMAND,
145 label="Peak demand",
146 sort_order=5,
147 line_kind=ResultLineKind.FINANCIAL_METRIC,
148 format=MetricFormat.NUMBER,
149 ),
150 _spec(
151 FinancialMetricKey.ANNUAL_DEPRECIATION,
152 label="Annual equipment depreciation",
153 sort_order=6,
154 line_kind=ResultLineKind.DEPRECIATION,
155 format=MetricFormat.MONEY_PER_YEAR,
156 key_financial_metric=True,
157 key_metric_sort_order=40,
158 ),
159 _spec(
160 FinancialMetricKey.DEPRECIATION_TAX_SHIELD,
161 label="Depreciation tax shield",
162 sort_order=7,
163 line_kind=ResultLineKind.DEPRECIATION,
164 format=MetricFormat.MONEY_PER_YEAR,
165 key_financial_metric=True,
166 key_metric_sort_order=50,
167 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
168 ),
169 _spec(
170 FinancialMetricKey.ANNUAL_OPEX,
171 label="Annual expenses",
172 sort_order=10,
173 line_kind=ResultLineKind.OPERATING,
174 format=MetricFormat.MONEY_PER_YEAR,
175 key_financial_metric=True,
176 key_metric_sort_order=80,
177 default_comparison_metric=True,
178 comparison_direction=MetricComparisonDirection.LOWER_IS_BETTER,
179 ),
180 _spec(
181 FinancialMetricKey.ANNUAL_REVENUE,
182 label="Annual revenue",
183 sort_order=11,
184 line_kind=ResultLineKind.OPERATING,
185 format=MetricFormat.MONEY_PER_YEAR,
186 ),
187 _spec(
188 FinancialMetricKey.ANNUAL_PROFIT,
189 label="Annual profit",
190 sort_order=12,
191 line_kind=ResultLineKind.OPERATING,
192 format=MetricFormat.MONEY_PER_YEAR,
193 ),
194 _spec(
195 FinancialMetricKey.ANNUAL_SAVINGS,
196 label="Annual savings",
197 sort_order=13,
198 line_kind=ResultLineKind.FINANCIAL_METRIC,
199 format=MetricFormat.MONEY_PER_YEAR,
200 baseline_dependent=True,
201 key_financial_metric=True,
202 key_metric_sort_order=10,
203 default_comparison_metric=True,
204 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
205 ),
206 _spec(
207 FinancialMetricKey.ANNUAL_NET_BENEFIT,
208 label="Annual net benefit",
209 sort_order=14,
210 line_kind=ResultLineKind.FINANCIAL_METRIC,
211 format=MetricFormat.MONEY_PER_YEAR,
212 baseline_dependent=True,
213 key_financial_metric=True,
214 key_metric_sort_order=20,
215 default_comparison_metric=True,
216 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
217 ),
218 _spec(
219 FinancialMetricKey.AFTER_TAX_ANNUAL_CASH_FLOW,
220 label="After-tax annual cash flow",
221 sort_order=15,
222 line_kind=ResultLineKind.FINANCIAL_METRIC,
223 format=MetricFormat.MONEY_PER_YEAR,
224 baseline_dependent=True,
225 key_financial_metric=True,
226 key_metric_sort_order=30,
227 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
228 ),
229 _spec(
230 FinancialMetricKey.INCREMENTAL_CAPEX,
231 label="Incremental capital cost",
232 sort_order=16,
233 line_kind=ResultLineKind.FINANCIAL_METRIC,
234 format=MetricFormat.MONEY,
235 baseline_dependent=True,
236 key_financial_metric=True,
237 key_metric_sort_order=70,
238 comparison_direction=MetricComparisonDirection.LOWER_IS_BETTER,
239 ),
240 _spec(
241 FinancialMetricKey.NPV,
242 label="NPV",
243 sort_order=17,
244 line_kind=ResultLineKind.FINANCIAL_METRIC,
245 format=MetricFormat.MONEY,
246 baseline_dependent=True,
247 key_financial_metric=True,
248 key_metric_sort_order=90,
249 default_comparison_metric=True,
250 maximum_fraction_digits=0,
251 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
252 ),
253 _spec(
254 FinancialMetricKey.SIMPLE_PAYBACK_YEARS,
255 label="Payback period",
256 sort_order=18,
257 line_kind=ResultLineKind.FINANCIAL_METRIC,
258 format=MetricFormat.YEARS,
259 baseline_dependent=True,
260 key_financial_metric=True,
261 key_metric_sort_order=0,
262 default_comparison_metric=True,
263 maximum_fraction_digits=4,
264 comparison_direction=MetricComparisonDirection.LOWER_IS_BETTER,
265 ),
266 _spec(
267 FinancialMetricKey.ROI_PERCENT,
268 label="ROI",
269 sort_order=19,
270 line_kind=ResultLineKind.FINANCIAL_METRIC,
271 format=MetricFormat.PERCENT,
272 baseline_dependent=True,
273 key_financial_metric=True,
274 key_metric_sort_order=100,
275 maximum_fraction_digits=2,
276 comparison_direction=MetricComparisonDirection.HIGHER_IS_BETTER,
277 ),
278)
280_SPECS_BY_KEY = {spec.key.value: spec for spec in FINANCIAL_METRICS}
281_SPECS_BY_ROW_KEY = {spec.row_key: spec for spec in FINANCIAL_METRICS}
284def financial_metric_spec(metric_key: str | FinancialMetricKey) -> FinancialMetricSpec | None:
285 return _SPECS_BY_KEY.get(_metric_key_value(metric_key))
288def required_financial_metric_spec(metric_key: str | FinancialMetricKey) -> FinancialMetricSpec:
289 spec = financial_metric_spec(metric_key)
290 if spec is None: 290 ↛ 291line 290 didn't jump to line 291 because the condition on line 290 was never true
291 raise KeyError(f"Unknown financial metric key: {metric_key}")
292 return spec
295def financial_metric_spec_for_row_key(row_key: str) -> FinancialMetricSpec | None:
296 return _SPECS_BY_ROW_KEY.get(row_key)
299def key_financial_metric_specs() -> tuple[FinancialMetricSpec, ...]:
300 return tuple(
301 sorted(
302 (spec for spec in FINANCIAL_METRICS if spec.key_financial_metric),
303 key=lambda spec: spec.key_metric_sort_order if spec.key_metric_sort_order is not None else spec.sort_order,
304 )
305 )
308def default_comparison_metric_specs() -> tuple[FinancialMetricSpec, ...]:
309 """Return the catalog-owned default metric set for comparison charts."""
310 return tuple(
311 spec
312 for spec in FINANCIAL_METRICS
313 if spec.default_comparison_metric
314 )
317def default_comparison_metric_keys() -> tuple[str, ...]:
318 """Return default comparison metric keys in catalog display order."""
319 return tuple(spec.key.value for spec in default_comparison_metric_specs())
322def key_financial_metric_rows_payload() -> list[dict[str, object]]:
323 """Return JSON-friendly key metric metadata for generated frontend data."""
324 return [
325 {
326 "key": spec.key.value,
327 "rowKey": spec.row_key,
328 "label": spec.label,
329 "format": spec.format.value,
330 "baselineDependent": spec.baseline_dependent,
331 "maximumFractionDigits": spec.maximum_fraction_digits,
332 "comparisonDirection": spec.comparison_direction.value,
333 "defaultComparisonMetric": spec.default_comparison_metric,
334 }
335 for spec in key_financial_metric_specs()
336 ]
339def _metric_key_value(metric_key: str | FinancialMetricKey) -> str:
340 return metric_key.value if isinstance(metric_key, FinancialMetricKey) else metric_key