French investment illustration: stylized SPV tax impact

Package cre.dcf

1 Purpose

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.

2 Start from a French operating case

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

3 Translate the French-like SPV into tax inputs

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

4 Read the yearly tax table

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

5 Compare pre-tax and after-tax equity cash flows

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

6 What this vignette captures and what it does not

This French illustration is useful because it shows a coherent workflow:

  1. build the investment case,
  2. extract a tax basis,
  3. define a country-like SPV parameterization,
  4. read the yearly tax consequences.

But it still has clear limits.

Captured:

Not captured:

7 Summary

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.

mirror server hosted at Truenetwork, Russian Federation.