tests/testthat/test-simple-api.R

test_that("debt_terms validates inputs and returns a typed object", {
  debt <- debt_terms()

  expect_s3_class(debt, "cre_debt_terms")
  expect_equal(debt$type, "bullet")
  expect_error(debt_terms(type = "foo"))
  expect_error(debt_terms(ltv = 1.2))
  expect_error(debt_terms(maturity = 0))
})

test_that("lease helpers build typed lease objects", {
  ev1 <- lease_event(start = 2025, end = 2026, rent = 200, vac = 0.05)
  ev2 <- lease_event(start = 2027, end = 2028, rent = 215, vac = 0, new_lease = TRUE, free_months = 3)
  unit <- lease_unit("A1", area_sqm = 1500, events = list(ev1, ev2))
  roll <- lease_roll(list(unit))

  expect_s3_class(ev1, "cre_lease_event")
  expect_s3_class(unit, "cre_lease_unit")
  expect_s3_class(roll, "cre_lease_roll")
  expect_equal(unit$unit, "A1")
  expect_length(roll$units, 1)

  expect_error(lease_event(start = 2026, end = 2025, rent = 200))
  expect_error(lease_unit("A1", area_sqm = 1500, events = list(list(start = 2025))))
  expect_error(lease_roll(list(list(unit = "raw"))))
})

test_that("analyst-oriented lease helpers and prints are informative", {
  vac <- vacancy_event(start = 2027, end = 2027, capex_sqm = 35)
  ren <- renewal_event(start = 2028, end = 2031, rent = 240, free_months = 3, capex_sqm = 20)
  unit <- lease_unit(
    "North",
    area_sqm = 1200,
    events = list(
      lease_event(start = 2025, end = 2026, rent = 220, vac = 0.05),
      vac,
      ren
    )
  )
  roll <- lease_roll(list(unit))

  expect_equal(vac$rent, 0)
  expect_equal(vac$vac, 1)
  expect_equal(vac$new_lease, 0)
  expect_equal(ren$new_lease, 1)

  snap_unit <- lease_roll_snapshot(unit)
  snap_roll <- lease_roll_snapshot(roll)
  expect_s3_class(snap_unit, "tbl_df")
  expect_s3_class(snap_roll, "tbl_df")
  expect_s3_class(summary(unit), "tbl_df")
  expect_s3_class(summary(roll), "tbl_df")
  expect_equal(snap_unit$event_count, 3)
  expect_equal(snap_unit$free_months_total, 3)
  expect_equal(snap_unit$capex_sqm_total, 55)
  expect_equal(snap_unit$reletting_count, 1)

  expect_output(print(vac), "cre_lease_event")
  expect_output(print(unit), "cre_lease_unit")
  expect_output(print(roll), "cre_lease_roll")
  expect_output(print(roll), "North")
})

test_that("deal_spec accepts exactly one income mode", {
  expect_s3_class(
    deal_spec(price = 10e6, entry_yield = 0.055),
    "cre_deal_spec"
  )
  expect_s3_class(
    deal_spec(price = 10e6, noi_y1 = 550000),
    "cre_deal_spec"
  )
  expect_s3_class(
    deal_spec(price = 10e6, rent_sqm = 220, area_sqm = 3000),
    "cre_deal_spec"
  )
  expect_s3_class(
    deal_spec(
      price = 10e6,
      lease_roll = lease_roll(list(
        lease_unit(
          "A1",
          area_sqm = 3000,
          events = list(lease_event(start = 2025, end = 2034, rent = 220, vac = 0.05))
        )
      )),
      purchase_year = 2025
    ),
    "cre_deal_spec"
  )

  expect_error(deal_spec(price = 10e6))
  expect_error(deal_spec(price = 10e6, entry_yield = 0.055, noi_y1 = 550000))
  expect_error(deal_spec(price = 10e6, rent_sqm = 220))
  expect_error(deal_spec(
    price = 10e6,
    rent_sqm = 220,
    area_sqm = 3000,
    lease_roll = lease_roll(list(
      lease_unit("A1", area_sqm = 3000, events = list(lease_event(start = 2025, end = 2034, rent = 220)))
    ))
  ))
})

test_that("deal_to_config produces a valid engine config", {
  deal_entry <- deal_spec(price = 10e6, entry_yield = 0.055, capex = c(0, 0, 1000, rep(0, 7)))
  cfg_entry <- deal_to_config(deal_entry)
  expect_no_error(cfg_validate(cfg_entry))
  expect_true(isTRUE(cfg_entry$top_down_noi))
  expect_equal(cfg_entry$disc_method, "risk_premium")
  expect_equal(cfg_entry$disc_rate_risk_premium$rf, deal_entry$discount_rate)

  deal_noi <- deal_spec(price = 10e6, noi_y1 = 550000)
  cfg_noi <- deal_to_config(deal_noi)
  expect_no_error(cfg_validate(cfg_noi))
  expect_false(isTRUE(cfg_noi$top_down_noi))
  expect_equal(cfg_noi$entry_yield, 550000 / 10e6)
  expect_length(cfg_noi$leases, 1)
})

test_that("analyze_deal reproduces the engine on entry-yield mode", {
  deal <- deal_spec(
    price = 10e6,
    entry_yield = 0.055,
    horizon_years = 7,
    discount_rate = 0.08,
    debt = debt_terms(ltv = 0.6, rate = 0.045, type = "bullet")
  )

  res_simple <- analyze_deal(deal)
  cfg <- deal_to_config(deal)
  res_engine <- run_case(config = cfg, ltv_base = "price_di")

  expect_equal(res_simple$all_equity$irr_project, res_engine$all_equity$irr_project, tolerance = 1e-10)
  expect_equal(res_simple$leveraged$irr_equity, res_engine$leveraged$irr_equity, tolerance = 1e-10)
  expect_equal(res_simple$cashflows$free_cash_flow, res_engine$cashflows$free_cash_flow)
})

test_that("analyze_deal supports direct NOI and amortising debt", {
  deal <- deal_spec(
    price = 9e6,
    noi_y1 = 540000,
    horizon_years = 6,
    discount_rate = 0.075,
    debt = debt_terms(ltv = 0.55, rate = 0.043, type = "amort", maturity = 6)
  )

  res_simple <- analyze_deal(deal)
  cfg <- deal_to_config(deal)
  res_engine <- run_case(config = cfg, ltv_base = "price_di")

  expect_equal(res_simple$config$debt_type, "amort")
  expect_equal(res_simple$all_equity$irr_project, res_engine$all_equity$irr_project, tolerance = 1e-10)
  expect_equal(res_simple$leveraged$irr_equity, res_engine$leveraged$irr_equity, tolerance = 1e-10)
})

test_that("analyze_deal supports a lease roll without YAML authoring", {
  roll <- lease_roll(list(
    lease_unit(
      "North",
      area_sqm = 1200,
      events = list(
        lease_event(start = 2025, end = 2026, rent = 220, vac = 0.08),
        lease_event(start = 2027, end = 2029, rent = 235, vac = 0, new_lease = TRUE, free_months = 3, capex_sqm = 24)
      )
    ),
    lease_unit(
      "South",
      area_sqm = 800,
      events = list(
        lease_event(start = 2025, end = 2029, rent = 205, vac = 0.03)
      )
    )
  ))

  deal <- deal_spec(
    price = 7.8e6,
    lease_roll = roll,
    purchase_year = 2025,
    horizon_years = 4,
    opex_sqm = 14,
    debt = debt_terms(ltv = 0.55, rate = 0.043, type = "bullet")
  )

  res_simple <- analyze_deal(deal)
  cfg <- deal_to_config(deal)
  res_engine <- run_case(config = cfg, ltv_base = "price_di")

  expect_equal(deal$income_mode, "lease_roll")
  expect_equal(length(cfg$leases), 2)
  expect_equal(res_simple$all_equity$irr_project, res_engine$all_equity$irr_project, tolerance = 1e-10)
  expect_equal(res_simple$leveraged$irr_equity, res_engine$leveraged$irr_equity, tolerance = 1e-10)

  snap <- asset_snapshot(res_simple)
  expect_equal(snap$area_sqm, 2000)
  expect_true(is.finite(snap$rent_sqm))
  expect_true(is.finite(snap$noi_y1))
  expect_true(all(c("year", "gei", "noi", "pbtcf", "opex", "capex") %in% names(deal_cashflows(res_simple, "operating"))))
})

test_that("print, summary and deal_cashflows work on simplified results", {
  deal <- deal_spec(
    price = 8e6,
    rent_sqm = 250,
    area_sqm = 2500,
    vacancy_rate = 0.05,
    opex_sqm = 12
  )

  expect_output(print(deal), "cre_deal_spec")
  expect_output(print(deal), "asset:")
  expect_output(print(deal), "GEI")

  res <- analyze_deal(deal)
  expect_output(print(res), "cre_deal_result")

  snap_deal <- asset_snapshot(deal)
  expect_s3_class(snap_deal, "tbl_df")
  expect_equal(snap_deal$price_per_sqm, 8e6 / 2500)
  expect_equal(snap_deal$gross_potential_rent_y1, 250 * 2500)
  expect_equal(snap_deal$gei_y1, 250 * 2500 * (1 - 0.05))

  snap_res <- asset_snapshot(res)
  year1 <- res$cashflows[res$cashflows$year == 1L, , drop = FALSE]
  expect_s3_class(snap_res, "tbl_df")
  expect_equal(snap_res$area_sqm, 2500)
  expect_equal(snap_res$gei_y1, year1$gei[[1]])
  expect_equal(snap_res$noi_y1, year1$noi[[1]])
  expect_equal(snap_res$pbtcf_y1, year1$pbtcf[[1]])

  smry <- summary(res)
  expect_s3_class(smry, "tbl_df")
  expect_true(all(c(
    "price", "area_sqm", "price_per_sqm", "rent_sqm", "vacancy_rate",
    "gross_potential_rent_y1", "gei_y1", "noi_y1", "pbtcf_y1",
    "horizon_years", "debt_type", "debt_ltv", "debt_rate",
    "irr_project", "irr_equity", "dscr_min", "ltv_max_forward", "ops_share", "tv_share"
  ) %in% names(smry)))

  expect_s3_class(deal_cashflows(res, "full"), "data.frame")
  expect_s3_class(deal_cashflows(res, "operating"), "data.frame")
  expect_s3_class(deal_cashflows(res, "all_equity"), "data.frame")
  expect_s3_class(deal_cashflows(res, "leveraged"), "data.frame")
  expect_s3_class(deal_cashflows(res, "comparison"), "data.frame")
  expect_true(all(c("year", "gei", "noi", "pbtcf", "opex", "capex") %in% names(deal_cashflows(res, "operating"))))
})

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.