Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dcaf/project/_compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion dcaf/project/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---


Expand Down