Before-tax by design: scope, portability, and future tax extensions

knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(cre.dcf)
library(dplyr)
library(tibble)

Purpose

This 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.

Why the core still stops at the before-tax level

The current public API is strongest where the manuals are most universal:

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.

What the core already gives us

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

The economic logic is already explicit:

This is one of the main reasons the package can add a fiscal layer later without rewriting the current core.

What the current tax_rate does and does not do

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

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:

So the package already acknowledges tax in the cost-of-capital sense, but not yet in the jurisdictional cash-flow sense.

Why this helps with multi-jurisdiction portability

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 current SPV-level tax helper

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

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.

A working generic SPV tax run

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))
tax_res$summary

This is enough to show the intended layering:

The yearly tax table now exposes columns such as:

It remains deliberately modest:

How to read the French fiscal-impact vignette

The package now includes a French investment vignette built on tax_run_spv().

That vignette should be read for what it is:

  1. an illustration of one country-like parameterization of the generic engine,
  2. a teaching-oriented bridge from before-tax DCF to SPV-level cash taxes,
  3. not a complete encoding of French tax law.

In other words, the French vignette is useful precisely because it sits on top of the generic architecture rather than driving the architecture itself.

Summary

The absence of a full tax engine in the current release is not a methodological weakness. It is a scope decision.

This keeps cre.dcf scientifically defensible today while supporting applied tax vignettes, including the current stylized French investment illustration built on tax_run_spv().



Try the cre.dcf package in your browser

Any scripts or data that you put into this service are public.

cre.dcf documentation built on April 10, 2026, 5:08 p.m.