tax_rate does and does not doThis vignette explains a deliberate scope choice in
cre.dcf: the package is currently designed as a
property-level, before-tax DCF engine.
That choice is methodological and practical at the same time.
The portability argument is an implementation choice inferred from those chapters. The textbooks justify a strong before-tax core; the package then keeps that core jurisdiction-agnostic on purpose.
The package now also includes a first generic SPV-level tax helper,
tax_run_spv(). The important design point is that this tax
layer sits on top of the before-tax core rather than replacing it.
The current public API is strongest where the manuals are most universal:
PBTCF),This is already a meaningful analytical perimeter. It supports property comparison, financing comparison, exit-dependence diagnostics, and lease analysis without hard-coding any national tax code into the engine.
By contrast, after-tax analysis quickly becomes specific to:
That is exactly why cre.dcf does not yet pretend to
return a fully jurisdiction-specific after-tax investment value. The new
tax helper is intentionally generic and stylized.
The present version already produces most of the building blocks that a future SPV-level tax layer would need.
cfg_path <- system.file("extdata", "preset_core.yml", package = "cre.dcf")
cfg <- yaml::read_yaml(cfg_path)
case <- run_case(cfg)
tax_basis_preview <- case$cashflows |>
select(year, gei, noi, pbtcf, capex, interest, sale_proceeds, equity_flow) |>
filter(year <= 4 | year == max(year))
knitr::kable(
tax_basis_preview,
digits = 0,
caption = "Current outputs that can feed a future SPV-level tax layer"
)| year | gei | noi | pbtcf | capex | interest | sale_proceeds | equity_flow |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | -30816000 |
| 1 | 2377950 | 2376000 | 2376000 | 0 | 451968 | 0 | 1924032 |
| 2 | 2401730 | 2399760 | 2399760 | 0 | 451968 | 0 | 1947792 |
| 3 | 2425747 | 2423758 | 2423758 | 0 | 451968 | 0 | 1971790 |
| 4 | 2450004 | 2447995 | 2447995 | 0 | 451968 | 0 | 1996027 |
| 10 | 2628946 | 2618283 | 2601877 | 16405 | 451968 | 49867061 | 31472970 |
The economic logic is already explicit:
gei captures effective income,noi captures operating income net of recurring
expenses,pbtcf captures the property-level cash flow before debt
and taxes,interest and sale_proceeds are already
separated in the consolidated table.This is one of the main reasons the package can add a fiscal layer later without rewriting the current core.
tax_rate does and does not doThe package still exposes a tax_rate inside the
WACC-oriented discount-rate blocks.
tpl <- dcf_spec_template()
tibble(
KE = tpl$disc_rate_wacc$KE,
KD = tpl$disc_rate_wacc$KD,
tax_rate = tpl$disc_rate_wacc$tax_rate
)## # A tibble: 1 × 3
## KE KD tax_rate
## <dbl> <dbl> <dbl>
## 1 0.08 0.04 0.28
That field is part of the discounting convention. It adjusts the debt leg in a WACC-style formula. It is not a cash-tax engine.
In the current version, the following statements are true:
disc_rate_wacc$tax_rate affects discount-rate
construction,scr_ratio can still act as a fallback in some
discounting workflows,So the package already acknowledges tax in the cost-of-capital sense, but not yet in the jurisdictional cash-flow sense.
A generic before-tax engine travels well because it focuses on the economics that are most stable across jurisdictions:
The parts that differ the most from one country to another can then be isolated in a future tax specification instead of being mixed into the core valuation engine.
This is especially important if the same package may later be used for:
The tax layer is optional and leaves run_case()
untouched.
The intended split is:
future_tax_blocks <- tibble::tribble(
~block, ~consumes_from_core, ~adds_from_tax_spec, ~target_output,
"Tax depreciation", "price, capex, holding period", "asset split, depreciation lives, start rule", "tax_depreciation",
"Interest deductibility", "interest", "deductibility rule", "deductible_interest, interest_disallowed",
"Simple corporate tax", "taxable base after adjustments", "statutory rate", "cash_is",
"Loss carryforwards", "negative taxable income", "carryforward rule", "loss_cf_open, loss_cf_used, loss_cf_close"
)
knitr::kable(
future_tax_blocks,
caption = "Target blocks for a future SPV-level tax layer"
)| block | consumes_from_core | adds_from_tax_spec | target_output |
|---|---|---|---|
| Tax depreciation | price, capex, holding period | asset split, depreciation lives, start rule | tax_depreciation |
| Interest deductibility | interest | deductibility rule | deductible_interest, interest_disallowed |
| Simple corporate tax | taxable base after adjustments | statutory rate | cash_is |
| Loss carryforwards | negative taxable income | carryforward rule | loss_cf_open, loss_cf_used, loss_cf_close |
For version 1 of that tax layer, the scope should remain intentionally narrow:
That scope is large enough to support realistic teaching cases and comparative illustrations, but still narrow enough to remain portable across stylized jurisdictions.
The key design principle is that the before-tax case is still the source object, and the tax layer consumes it.
tax_basis <- tax_basis_spv(case)
tax_spec <- tax_spec_spv(
corp_tax_rate = 0.25,
depreciation_spec = depreciation_spec(
acquisition_split = tibble::tribble(
~bucket, ~share, ~life_years, ~method, ~depreciable,
"land", 0.20, NA, "none", FALSE,
"building", 0.65, 30, "straight_line", TRUE,
"fitout", 0.15, 10, "straight_line", TRUE
),
capex_bucket = "fitout",
start_rule = "full_year"
),
interest_rule = interest_rule(mode = "full"),
loss_rule = loss_rule(carryforward = TRUE, carryforward_years = Inf)
)
tax_res <- tax_run_spv(tax_basis, tax_spec)
tax_res$tax_table |>
select(
year, noi, tax_depreciation, deductible_interest,
taxable_income_pre_losses, loss_cf_open, loss_cf_used,
cash_is, after_tax_equity_cf
) |>
filter(year <= 4 | year == max(year))## # A tibble: 6 × 9
## year noi tax_depreciation deductible_interest taxable_income_pre_losses
## <int> <dbl> <dbl> <dbl> <dbl>
## 1 0 0 0 0 0
## 2 1 2376000 1760000 451968 164032
## 3 2 2399760 1760000 451968 187792
## 4 3 2423758. 1760000 451968 211790.
## 5 4 2447995. 1760000 451968 236027.
## 6 10 2618283. 1766465. 451968 19818340.
## # ℹ 4 more variables: loss_cf_open <dbl>, loss_cf_used <dbl>, cash_is <dbl>,
## # after_tax_equity_cf <dbl>
## # A tibble: 1 × 7
## corp_tax_rate acquisition_price total_tax_depreciation total_cash_is
## <dbl> <dbl> <dbl> <dbl>
## 1 0.25 48000000 17616083. 5554632.
## # ℹ 3 more variables: total_loss_generated <dbl>, final_loss_cf <dbl>,
## # total_after_tax_equity_cf <dbl>
This is enough to show the intended layering:
tax_basis_spv() extracts a tax basis from that
case,tax_run_spv() adds depreciation, deductible interest,
simple CIT, and loss carryforwards.The yearly tax table now exposes columns such as:
tax_depreciation,deductible_interest,interest_disallowed,taxable_income_pre_losses,loss_cf_open,loss_cf_used,loss_cf_close,cash_is,after_tax_equity_cf.It remains deliberately modest:
The package now includes a French investment vignette built on
tax_run_spv().
That vignette should be read for what it is:
In other words, the French vignette is useful precisely because it sits on top of the generic architecture rather than driving the architecture itself.
The absence of a full tax engine in the current release is not a methodological weakness. It is a scope decision.
tax_rate input is part of discounting, not
a fiscal cash-flow model.tax_run_spv() helper stays optional, generic,
and jurisdiction-agnostic at the API level.This keeps cre.dcf scientifically defensible today while
supporting applied tax vignettes, including the current stylized French
investment illustration built on tax_run_spv().