diff --git a/dcaf/project/_compiler.py b/dcaf/project/_compiler.py index 77ffc42..0e306dd 100644 --- a/dcaf/project/_compiler.py +++ b/dcaf/project/_compiler.py @@ -293,6 +293,7 @@ def compile(self) -> ProjectAnalysis: tax_rate=self.config.tax_rate, levelized_revenue_basis=CashFlowGroup(levelized_revenue_basis.streams), levelized_cost_escalation_rate=self.infer_levelized_cost_escalation_rate(), + tax_allow_refund=self.config.tax_allow_refund, ) def validate_generation_revenue_configuration(self) -> None: diff --git a/dcaf/project/analysis.py b/dcaf/project/analysis.py index 4660cdb..30d567b 100644 --- a/dcaf/project/analysis.py +++ b/dcaf/project/analysis.py @@ -227,6 +227,9 @@ class ProjectAnalysis: Inferred constant annual escalation rate used to construct a synthetic unit-price revenue basis when no market revenue basis is configured and the caller does not supply an explicit escalation policy. + tax_allow_refund : bool + Whether negative taxable income generates a tax refund. This policy is + preserved when computing counterfactual taxes for the interest tax shield. """ timeline: ProjectTimeline @@ -238,6 +241,7 @@ class ProjectAnalysis: tax_rate: float | None levelized_revenue_basis: CashFlowGroup[str] | None = None levelized_cost_escalation_rate: float | None = None + tax_allow_refund: bool = False @property def cashflows(self) -> CashFlowStream: @@ -459,7 +463,10 @@ def _interest_tax_shield(self) -> CashFlowStream: taxable_revenue, deductions_without_interest, label="Taxable Income Before Interest" ) taxes_without_interest = tax_liability( - taxable_income_without_interest, tax_rate=self.tax_rate, label="Taxes Before Interest" + taxable_income_without_interest, + tax_rate=self.tax_rate, + label="Taxes Before Interest", + allow_refund=self.tax_allow_refund, ) actual_by_date = self.taxes.group_by(lambda flow: flow.date).aggregate(lambda s: s.sum()) hypothetical_by_date = taxes_without_interest.group_by(lambda flow: flow.date).aggregate( diff --git a/tests/unit/test_project.py b/tests/unit/test_project.py index aa9878a..fe040b8 100644 --- a/tests/unit/test_project.py +++ b/tests/unit/test_project.py @@ -1192,6 +1192,49 @@ def test_project_pro_forma_groups_categories_and_computes_subtotals(): assert "project:tax_liability" not in row_map +@pytest.mark.parametrize( + ("annual_rate", "expected_refund", "expected_shield"), + [(0.0, 20.0, 0.0), (0.10, 22.0, 2.0)], +) +def test_project_pro_forma_interest_tax_shield_preserves_refund_policy( + annual_rate, + expected_refund, + expected_shield, +): + schedule = AmortizationSchedule.build( + principal=100.0, + annual_rate=annual_rate, + term=1, + start_date=date(2026, 1, 1), + frequency="year", + ) + analysis = ( + EnergyProject() + .add_cashflow_stream( + name="deduction", + stream=CashFlowStream( + [ + CashFlow( + -100.0, + date(2026, 1, 1), + pro_forma_category="operating_cost", + tax_treatment="deductible", + ) + ] + ), + ) + .debt_schedule(schedule=schedule) + .tax(rate=0.20, allow_refund=True) + .analyze() + ) + + row_map = analysis.pro_forma().row_map() + + assert row_map["Taxes"] == pytest.approx((expected_refund,)) + assert row_map["Financing Interest"] == pytest.approx((-100.0 * annual_rate,)) + assert row_map["Interest Tax Shield"] == pytest.approx((expected_shield,)) + + # --- Multiple named cost items ---