tests/testthat/test-methodological-helpers.R

test_that("lease_effective_rent reproduces a constant annuity", {
  out <- lease_effective_rent(
    cashflows = rep(100, 5),
    discount_rate = 0.08,
    timing = "arrears",
    perspective = "landlord"
  )

  expect_equal(out$equivalent_annuity, 100, tolerance = 1e-10)
  expect_equal(out$effective_rent, 100, tolerance = 1e-10)
})

test_that("lease_effective_rent captures concessions and area normalization", {
  base <- lease_effective_rent(
    cashflows = rep(100, 5),
    discount_rate = 0.08,
    area = 10,
    timing = "arrears",
    perspective = "landlord"
  )
  concessive <- lease_effective_rent(
    cashflows = c(0, 100, 100, 100, 100),
    discount_rate = 0.08,
    area = 10,
    timing = "arrears",
    perspective = "landlord"
  )

  expect_lt(concessive$effective_rent, base$effective_rent)
  expect_equal(base$effective_rent_per_area, base$effective_rent / 10, tolerance = 1e-10)
})

test_that("lease_effective_rent stays symmetric across landlord and tenant perspectives", {
  landlord <- lease_effective_rent(
    cashflows = rep(100, 4),
    discount_rate = 0.07,
    timing = "arrears",
    perspective = "landlord"
  )
  tenant <- lease_effective_rent(
    cashflows = rep(-100, 4),
    discount_rate = 0.07,
    timing = "arrears",
    perspective = "tenant"
  )

  expect_equal(abs(landlord$effective_rent), abs(tenant$effective_rent), tolerance = 1e-10)
  expect_equal(landlord$effective_rent, -tenant$effective_rent, tolerance = 1e-10)
})

test_that("underwrite_loan identifies an LTV binding constraint", {
  uw <- underwrite_loan(
    noi = 1e6,
    value = 5e6,
    rate_annual = 0.05,
    maturity = 5,
    type = "bullet",
    dscr_min = 1.25,
    ltv_max = 0.50,
    debt_yield_min = 0.08
  )

  expect_equal(uw$binding_constraint, "ltv")
  expect_equal(uw$max_loan, uw$max_loan_ltv, tolerance = 1e-10)
  expect_equal(uw$implied_ltv, 0.5, tolerance = 1e-10)
  expect_equal(
    uw$payment_year1,
    debt_built_schedule(uw$max_loan, 0.05, 5, type = "bullet")$payment[2],
    tolerance = 1e-10
  )
})

test_that("underwrite_loan identifies a DSCR binding constraint", {
  uw <- underwrite_loan(
    noi = 5e5,
    value = 20e6,
    rate_annual = 0.06,
    maturity = 5,
    type = "amort",
    dscr_min = 1.25,
    ltv_max = 0.80,
    debt_yield_min = 0.03
  )

  expect_equal(uw$binding_constraint, "dscr")
  expect_equal(uw$max_loan, uw$max_loan_dscr, tolerance = 1e-10)
  expect_gte(uw$implied_dscr, 1.25 - 1e-6)
  expect_equal(
    uw$payment_year1,
    debt_built_schedule(uw$max_loan, 0.06, 5, type = "amort")$payment[2],
    tolerance = 1e-10
  )
})

test_that("underwrite_loan identifies a debt-yield binding constraint", {
  uw <- underwrite_loan(
    noi = 5e5,
    value = 20e6,
    rate_annual = 0.03,
    maturity = 5,
    type = "bullet",
    dscr_min = 1.25,
    ltv_max = 0.90,
    debt_yield_min = 0.08
  )

  expect_equal(uw$binding_constraint, "debt_yield")
  expect_equal(uw$max_loan, uw$max_loan_debt_yield, tolerance = 1e-10)
  expect_equal(uw$implied_debt_yield, 0.08, tolerance = 1e-8)
})

test_that("compare_financing_scenarios surfaces operations and terminal shares", {
  dcf <- dcf_calculate(1e7, 0.05, 0.055, 5, 0.07)
  cmp <- compare_financing_scenarios(
    dcf_res = dcf,
    acq_price = 1e7,
    ltv = 0.60,
    rate = 0.045,
    maturity = 5
  )

  expect_true(all(c("ops_share", "tv_share") %in% names(cmp$summary)))
  expect_true(all(abs(cmp$summary$ops_share + cmp$summary$tv_share - 1) < 1e-10))
})

test_that("tax_run_spv computes depreciation, exit gain, taxes, and after-tax equity flows", {
  basis <- tibble::tibble(
    year = 0:4,
    noi = c(0, 140, 150, 160, 170),
    capex = c(0, 0, 20, 0, 0),
    interest = c(0, 30, 25, 20, 0),
    sale_proceeds = c(0, 0, 0, 0, 900),
    pre_tax_equity_cf = c(-1000, 110, 105, 120, 950)
  )

  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.80,         4,    "straight_line",  TRUE
      ),
      capex_bucket = "building",
      start_rule = "full_year"
    )
  )

  out <- tax_run_spv(basis, spec, acquisition_price = 1000)
  tbl <- out$tax_table

  expect_equal(tbl$tax_depreciation, c(0, 200, 205, 205, 205), tolerance = 1e-10)
  expect_equal(tbl$book_value_close_pre_exit[5], 205, tolerance = 1e-10)
  expect_equal(tbl$taxable_exit_gain_loss[5], 695, tolerance = 1e-10)
  expect_equal(tbl$loss_cf_open[5], 235, tolerance = 1e-10)
  expect_equal(tbl$loss_cf_used[5], 235, tolerance = 1e-10)
  expect_equal(tbl$taxable_income_post_losses[5], 425, tolerance = 1e-10)
  expect_equal(tbl$cash_is[5], 106.25, tolerance = 1e-10)
  expect_equal(tbl$after_tax_equity_cf[5], 843.75, tolerance = 1e-10)
  expect_equal(out$summary$total_cash_is, 106.25, tolerance = 1e-10)
})

test_that("tax_run_spv supports next-year depreciation and capped use of losses", {
  basis <- tibble::tibble(
    year = 0:3,
    noi = c(0, 40, 300, 500),
    capex = c(0, 0, 0, 0),
    interest = c(0, 0, 0, 0),
    sale_proceeds = c(0, 0, 0, 0)
  )

  spec <- tax_spec_spv(
    corp_tax_rate = 0.30,
    depreciation_spec = depreciation_spec(
      acquisition_split = tibble::tribble(
        ~bucket,    ~share, ~life_years, ~method,          ~depreciable,
        "land",      0.20,        NA,    "none",           FALSE,
        "building",  0.80,         2,    "straight_line",  TRUE
      ),
      capex_bucket = "building",
      start_rule = "next_year"
    ),
    loss_rule = loss_rule(
      carryforward = TRUE,
      carryforward_years = Inf,
      offset_cap_pct = 0.50
    )
  )

  out <- tax_run_spv(basis, spec, acquisition_price = 1000)
  tbl <- out$tax_table

  expect_equal(tbl$tax_depreciation, c(0, 0, 400, 400), tolerance = 1e-10)
  expect_equal(tbl$loss_generated[2], 0, tolerance = 1e-10)
  expect_equal(tbl$loss_generated[3], 100, tolerance = 1e-10)
  expect_equal(tbl$loss_cf_open[4], 100, tolerance = 1e-10)
  expect_equal(tbl$loss_cf_used[4], 50, tolerance = 1e-10)
  expect_equal(tbl$taxable_income_post_losses[4], 50, tolerance = 1e-10)
  expect_equal(tbl$cash_is[4], 15, tolerance = 1e-10)
})

test_that("tax_basis_spv extracts a tax basis from a pre-tax case", {
  deal <- deal_spec(
    price = 10e6,
    entry_yield = 0.055,
    horizon_years = 5,
    debt = debt_terms(ltv = 0.5, rate = 0.04, type = "bullet")
  )

  res <- analyze_deal(deal)
  basis <- tax_basis_spv(res)

  expect_true(all(c(
    "year", "noi", "capex", "interest", "sale_proceeds",
    "pre_tax_equity_cf", "acquisition_price"
  ) %in% names(basis)))
  expect_equal(length(unique(basis$acquisition_price)), 1L)
  expect_equal(unique(basis$acquisition_price), res$pricing$price_ht, tolerance = 1e-8)
})

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.