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

1"""Central financial metric catalog shared by results and comparisons.""" 

2 

3from __future__ import annotations 

4 

5from enum import StrEnum 

6 

7from pydantic import BaseModel, ConfigDict 

8 

9from Economics.shared.choices import ResultLineKind 

10 

11 

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" 

31 

32 

33class MetricFormat(StrEnum): 

34 MONEY = "money" 

35 MONEY_PER_YEAR = "money_per_year" 

36 NUMBER = "number" 

37 PERCENT = "percent" 

38 YEARS = "years" 

39 

40 

41class MetricComparisonDirection(StrEnum): 

42 """Business meaning for ranking metric values inside study comparisons.""" 

43 

44 HIGHER_IS_BETTER = "higher_is_better" 

45 LOWER_IS_BETTER = "lower_is_better" 

46 NOT_RANKED = "not_ranked" 

47 

48 

49class FinancialMetricSpec(BaseModel): 

50 """Display and calculation metadata for one financial metric.""" 

51 

52 model_config = ConfigDict(frozen=True) 

53 

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 ) 

68 

69 

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 ) 

101 

102 

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) 

279 

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} 

282 

283 

284def financial_metric_spec(metric_key: str | FinancialMetricKey) -> FinancialMetricSpec | None: 

285 return _SPECS_BY_KEY.get(_metric_key_value(metric_key)) 

286 

287 

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 

293 

294 

295def financial_metric_spec_for_row_key(row_key: str) -> FinancialMetricSpec | None: 

296 return _SPECS_BY_ROW_KEY.get(row_key) 

297 

298 

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 ) 

306 

307 

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 ) 

315 

316 

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()) 

320 

321 

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 ] 

337 

338 

339def _metric_key_value(metric_key: str | FinancialMetricKey) -> str: 

340 return metric_key.value if isinstance(metric_key, FinancialMetricKey) else metric_key