This vignette illustrates how tax_run_spv() can be used
on a French office investment case.
It is intentionally a stylized French-like SPV illustration, not a legal or tax opinion. As of April 1, 2026, the normal French corporate income-tax rate is 25%, following the official French tax administration’s published corporate-tax guidance consulted for this vignette.
The rest of the tax assumptions below are simulated package inputs
chosen to remain within the current scope of
tax_run_spv():
The point of the vignette is therefore methodological: show how a French case can be parameterized in the current generic engine without pretending to replicate the full French tax code.
We use the package’s preset_core.yml, which already
describes a stylized Paris 8e office investment.
cfg_path <- system.file("extdata", "preset_core.yml", package = "cre.dcf")
cfg <- yaml::read_yaml(cfg_path)
case <- run_case(cfg)
bullet_summary <- case$comparison$summary |>
filter(scenario == "debt_bullet") |>
transmute(
scenario,
irr_project,
irr_equity,
min_dscr,
max_ltv_forward,
ops_share,
tv_share
)
knitr::kable(
bullet_summary,
digits = 3,
caption = "Before-tax baseline for the stylized French core-office case"
)| scenario | irr_project | irr_equity | min_dscr | max_ltv_forward | ops_share | tv_share |
|---|---|---|---|---|---|---|
| debt_bullet | 0.046 | 0.062 | 5.257 | 0.449 | 0.37 | 0.63 |
At this stage, the core DCF is still entirely before tax. The tax layer comes next and consumes the consolidated cash-flow table.
The current tax engine needs two objects:
tax_basis <- tax_basis_spv(case)
tax_assumptions <- tibble::tribble(
~item, ~value, ~comment,
"Corporate income tax", "25%", "Official French normal CIT rate as of April 1, 2026",
"Land share", "20%", "Stylized non-depreciable portion",
"Building share", "65%", "Stylized depreciable structure",
"Fit-out share", "15%", "Stylized depreciable tenant-improvement bucket",
"Building life", "30 years", "Stylized straight-line life",
"Fit-out life", "10 years", "Stylized straight-line life",
"Interest deductibility", "full", "Current engine scope, not full French law",
"Loss carryforward", "on", "Stylized carryforward allowed",
"Offset cap", "50%", "Stylized cap used to keep the rule conservative"
)
knitr::kable(
tax_assumptions,
caption = "French-like tax assumptions used in this vignette"
)| item | value | comment |
|---|---|---|
| Corporate income tax | 25% | Official French normal CIT rate as of April 1, 2026 |
| Land share | 20% | Stylized non-depreciable portion |
| Building share | 65% | Stylized depreciable structure |
| Fit-out share | 15% | Stylized depreciable tenant-improvement bucket |
| Building life | 30 years | Stylized straight-line life |
| Fit-out life | 10 years | Stylized straight-line life |
| Interest deductibility | full | Current engine scope, not full French law |
| Loss carryforward | on | Stylized carryforward allowed |
| Offset cap | 50% | Stylized cap used to keep the rule conservative |
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,
offset_cap_pct = 0.50
)
)
tax_res <- tax_run_spv(tax_basis, tax_spec)The yearly table below is the main output of
tax_run_spv().
tax_view <- 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
)
knitr::kable(
tax_view,
digits = 0,
caption = "Stylized French SPV tax table"
)| year | noi | tax_depreciation | deductible_interest | taxable_income_pre_losses | loss_cf_open | loss_cf_used | cash_is | after_tax_equity_cf |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | -30816000 |
| 1 | 2376000 | 1760000 | 451968 | 164032 | 0 | 0 | 41008 | 1883024 |
| 2 | 2399760 | 1760000 | 451968 | 187792 | 0 | 0 | 46948 | 1900844 |
| 3 | 2423758 | 1760000 | 451968 | 211790 | 0 | 0 | 52947 | 1918842 |
| 4 | 2447995 | 1760000 | 451968 | 236027 | 0 | 0 | 59007 | 1937020 |
| 5 | 2472475 | 1760000 | 451968 | 260507 | 0 | 0 | 65127 | 1955380 |
| 6 | 2497200 | 1760000 | 451968 | 285232 | 0 | 0 | 71308 | 1973924 |
| 7 | 2541279 | 1761592 | 451968 | 327719 | 0 | 0 | 81930 | 1991459 |
| 8 | 2566692 | 1763200 | 451968 | 351524 | 0 | 0 | 87881 | 2010761 |
| 9 | 2592359 | 1764825 | 451968 | 375566 | 0 | 0 | 93892 | 2030257 |
| 10 | 2618283 | 1766465 | 451968 | 19818340 | 0 | 0 | 4954585 | 26518385 |
This is the main reading grid:
tax_depreciation transforms acquisition basis and capex
into fiscal charges,deductible_interest removes the debt cost that is
deductible under the simplified rule,taxable_income_pre_losses is the taxable base before
using prior losses,loss_cf_open and loss_cf_used show how
prior tax losses are mobilized,cash_is is the annual corporate income tax paid by the
SPV,after_tax_equity_cf is the equity cash flow net of that
annual tax.The generic engine does not yet build a complete French investor-level valuation. It does, however, let us measure the tax drag at the SPV level.
equity_bridge <- tax_res$tax_table |>
select(year, pre_tax_equity_cf, cash_is, after_tax_equity_cf)
knitr::kable(
equity_bridge,
digits = 0,
caption = "Bridge from pre-tax to after-tax equity cash flows"
)| year | pre_tax_equity_cf | cash_is | after_tax_equity_cf |
|---|---|---|---|
| 0 | -30816000 | 0 | -30816000 |
| 1 | 1924032 | 41008 | 1883024 |
| 2 | 1947792 | 46948 | 1900844 |
| 3 | 1971790 | 52947 | 1918842 |
| 4 | 1996027 | 59007 | 1937020 |
| 5 | 2020507 | 65127 | 1955380 |
| 6 | 2045232 | 71308 | 1973924 |
| 7 | 2073388 | 81930 | 1991459 |
| 8 | 2098642 | 87881 | 2010761 |
| 9 | 2124148 | 93892 | 2030257 |
| 10 | 31472970 | 4954585 | 26518385 |
We can also compare the before-tax leveraged IRR with a stylized
after-tax SPV equity IRR computed on
after_tax_equity_cf.
pre_tax_equity_irr <- case$leveraged$irr_equity
after_tax_equity_irr <- irr_safe(tax_res$tax_table$after_tax_equity_cf)
tax_highlights <- tibble(
pre_tax_equity_irr = pre_tax_equity_irr,
after_tax_spv_equity_irr = after_tax_equity_irr,
total_cash_is = tax_res$summary$total_cash_is,
total_tax_depreciation = tax_res$summary$total_tax_depreciation,
exit_year_cash_is = tax_res$tax_table$cash_is[
tax_res$tax_table$year == max(tax_res$tax_table$year)
]
)
knitr::kable(
tax_highlights,
digits = 3,
caption = "Stylized before-tax versus after-tax SPV reading"
)| pre_tax_equity_irr | after_tax_spv_equity_irr | total_cash_is | total_tax_depreciation | exit_year_cash_is |
|---|---|---|---|---|
| 0.062 | 0.047 | 5554632 | 17616083 | 4954585 |
In this example:
cash_is,This French illustration is useful because it shows a coherent workflow:
But it still has clear limits.
Captured:
Not captured:
This vignette shows how cre.dcf can already support a
realistic French-style teaching case without hard-coding French tax law
into the package core.
tax_run_spv() adds a stylized SPV tax reading on top of
it.