Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/added/lbtt-ltt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Scottish LBTT (Land and Buildings Transaction Tax) and Welsh LTT (Land Transaction Tax) as devolved replacements for SDLT. Property transactions now dispatch by region: Scotland → LBTT, Wales → LTT, England + NI → SDLT. New `lbtt` and `ltt` reform parameters in the Python wrapper, 2025/26 residential bands sourced from the Land and Buildings Transaction Tax (Tax Rates and Tax Bands) (Scotland) Order 2015 and the Land Transaction Tax (Tax Bands and Tax Rates) (Wales) Regulations 2018, and a new `baseline_property_transaction_tax` / `reform_property_transaction_tax` per-household microdata column.
2 changes: 2 additions & 0 deletions interfaces/python/policyengine_uk_compiled/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ class Parameters(BaseModel):
income_related_benefits: Optional[IncomeRelatedBenefitParams] = None
capital_gains_tax: Optional[CapitalGainsTaxParams] = None
stamp_duty: Optional[StampDutyParams] = None
lbtt: Optional[StampDutyParams] = None
ltt: Optional[StampDutyParams] = None
wealth_tax: Optional[WealthTaxParams] = None
labour_supply: Optional[LabourSupplyParams] = None

Expand Down
27 changes: 26 additions & 1 deletion parameters/2025_26.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ capital_gains_tax:
realisation_rate: 0.50

stamp_duty:
# Finance Act 2003 s.55, as amended; bands from 1 April 2025
# SDLT — England + NI. Finance Act 2003 s.55, as amended; bands from 1 April 2025
bands:
- { rate: 0.0, threshold: 0 }
- { rate: 0.02, threshold: 125001 }
Expand All @@ -256,6 +256,31 @@ stamp_duty:
- { rate: 0.12, threshold: 1500001 }
annual_purchase_probability: 0.043 # ~1/23 year average holding period

lbtt:
# LBTT — Scotland's devolved replacement for SDLT. Land and Buildings Transaction
# Tax (Tax Rates and Tax Bands) (Scotland) Order 2015 (SSI 2015/126), Schedule.
# Residential primary-residence bands.
bands:
- { rate: 0.0, threshold: 0 }
- { rate: 0.02, threshold: 145001 }
- { rate: 0.05, threshold: 250001 }
- { rate: 0.10, threshold: 325001 }
- { rate: 0.12, threshold: 750001 }
annual_purchase_probability: 0.043

ltt:
# LTT — Wales's devolved replacement for SDLT. Land Transaction Tax (Tax Bands
# and Tax Rates) (Wales) Regulations 2018 (WSI 2018/128), Schedule.
# Residential primary-residence bands.
bands:
- { rate: 0.0, threshold: 0 }
- { rate: 0.035, threshold: 180001 }
- { rate: 0.05, threshold: 250001 }
- { rate: 0.075, threshold: 400001 }
- { rate: 0.10, threshold: 750001 }
- { rate: 0.12, threshold: 1500001 }
annual_purchase_probability: 0.043

wealth_tax:
# Hypothetical — no current UK legislation. Disabled by default.
# Wealth Tax Commission (2020) proposed 1% above £10m.
Expand Down
4 changes: 4 additions & 0 deletions src/data/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,10 +553,12 @@ fn write_microdata_csv_households<W: std::io::Write>(
// ── Baseline outputs ──
"baseline_net_income", "baseline_gross_income",
"baseline_total_tax", "baseline_total_benefits",
"baseline_property_transaction_tax",
"baseline_equivalisation_factor", "baseline_equivalised_net_income",
// ── Reform outputs ──
"reform_net_income", "reform_gross_income",
"reform_total_tax", "reform_total_benefits",
"reform_property_transaction_tax",
"reform_equivalisation_factor", "reform_equivalised_net_income",
])?;

Expand All @@ -576,13 +578,15 @@ fn write_microdata_csv_households<W: std::io::Write>(
format!("{:.2}", bl.gross_income),
format!("{:.2}", bl.total_tax),
format!("{:.2}", bl.total_benefits),
format!("{:.2}", bl.stamp_duty),
format!("{:.4}", bl.equivalisation_factor),
format!("{:.2}", bl.equivalised_net_income),
// Reform
format!("{:.2}", rf.net_income),
format!("{:.2}", rf.gross_income),
format!("{:.2}", rf.total_tax),
format!("{:.2}", rf.total_benefits),
format!("{:.2}", rf.stamp_duty),
format!("{:.4}", rf.equivalisation_factor),
format!("{:.2}", rf.equivalised_net_income),
])?;
Expand Down
13 changes: 9 additions & 4 deletions src/engine/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,15 @@ impl Simulation {
.map(|&pid| person_results[pid].capital_gains_tax)
.sum();

// Stamp duty (annualised)
let stamp_duty = self.parameters.stamp_duty.as_ref()
.map(|p| variables::wealth_taxes::calculate_stamp_duty(hh, p))
.unwrap_or(0.0);
// Property transaction tax (annualised): SDLT in England/NI, LBTT in
// Scotland, LTT in Wales. Stored on the household result as
// `stamp_duty` for backwards compatibility.
let stamp_duty = variables::wealth_taxes::calculate_property_transaction_tax(
hh,
self.parameters.stamp_duty.as_ref(),
self.parameters.lbtt.as_ref(),
self.parameters.ltt.as_ref(),
);

// Wealth tax
let wealth_tax = self.parameters.wealth_tax.as_ref()
Expand Down
10 changes: 9 additions & 1 deletion src/parameters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,17 @@ pub struct Parameters {
/// Capital gains tax. TCGA 1992; 18%/24% from October 2024 Budget.
#[serde(default)]
pub capital_gains_tax: Option<CapitalGainsTaxParams>,
/// Stamp duty land tax on residential property. FA 2003 s.55.
/// Stamp duty land tax on residential property (England + NI). FA 2003 s.55.
#[serde(default)]
pub stamp_duty: Option<StampDutyParams>,
/// Land and Buildings Transaction Tax — Scotland's devolved replacement for
/// SDLT. Land and Buildings Transaction Tax (Scotland) Act 2013, s.24.
#[serde(default)]
pub lbtt: Option<StampDutyParams>,
/// Land Transaction Tax — Wales's devolved replacement for SDLT.
/// Land Transaction Tax and Anti-avoidance of Devolved Taxes (Wales) Act 2017.
#[serde(default)]
pub ltt: Option<StampDutyParams>,
/// Annual wealth tax (hypothetical — disabled by default).
#[serde(default)]
pub wealth_tax: Option<WealthTaxParams>,
Expand Down
146 changes: 145 additions & 1 deletion src/variables/wealth_taxes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::engine::entities::{Household, Person};
use crate::engine::entities::{Household, Person, Region};
use crate::parameters::{CouncilTaxParams, CapitalGainsTaxParams, StampDutyParams, WealthTaxParams};

/// Determine the council tax band (0=A .. 7=H) from a 1991 property value.
Expand Down Expand Up @@ -82,6 +82,30 @@ pub fn calculate_stamp_duty(hh: &Household, params: &StampDutyParams) -> f64 {
sdlt * params.annual_purchase_probability
}

/// Calculate annualised property-transaction tax for a household, dispatching
/// to the regime that applies in the household's region.
///
/// - Scotland → LBTT (Land and Buildings Transaction Tax (Scotland) Act 2013)
/// - Wales → LTT (Land Transaction Tax and Anti-avoidance of Devolved Taxes (Wales) Act 2017)
/// - elsewhere (England + NI) → SDLT (Finance Act 2003 s.55)
///
/// Each parameter argument is optional; the function returns 0.0 when the
/// regime that would apply is unset (e.g. no LBTT params loaded for a Scottish
/// household), matching the existing behaviour for missing SDLT params.
pub fn calculate_property_transaction_tax(
hh: &Household,
sdlt: Option<&StampDutyParams>,
lbtt: Option<&StampDutyParams>,
ltt: Option<&StampDutyParams>,
) -> f64 {
let params = match hh.region {
Region::Scotland => lbtt,
Region::Wales => ltt,
_ => sdlt,
};
params.map(|p| calculate_stamp_duty(hh, p)).unwrap_or(0.0)
}

/// Calculate annual wealth tax for a household.
///
/// Hypothetical flat-rate tax on net wealth above a threshold.
Expand Down Expand Up @@ -197,6 +221,126 @@ mod tests {
assert!((sdlt - 15000.0).abs() < 1.0);
}

fn make_sdlt() -> StampDutyParams {
StampDutyParams {
bands: vec![
StampDutyBand { rate: 0.0, threshold: 0.0 },
StampDutyBand { rate: 0.02, threshold: 125001.0 },
StampDutyBand { rate: 0.05, threshold: 250001.0 },
StampDutyBand { rate: 0.10, threshold: 925001.0 },
StampDutyBand { rate: 0.12, threshold: 1500001.0 },
],
annual_purchase_probability: 1.0,
}
}

fn make_lbtt() -> StampDutyParams {
// Scotland 2025/26 (residential).
StampDutyParams {
bands: vec![
StampDutyBand { rate: 0.0, threshold: 0.0 },
StampDutyBand { rate: 0.02, threshold: 145001.0 },
StampDutyBand { rate: 0.05, threshold: 250001.0 },
StampDutyBand { rate: 0.10, threshold: 325001.0 },
StampDutyBand { rate: 0.12, threshold: 750001.0 },
],
annual_purchase_probability: 1.0,
}
}

fn make_ltt() -> StampDutyParams {
// Wales 2025/26 (residential primary).
StampDutyParams {
bands: vec![
StampDutyBand { rate: 0.0, threshold: 0.0 },
StampDutyBand { rate: 0.035, threshold: 180001.0 },
StampDutyBand { rate: 0.05, threshold: 250001.0 },
StampDutyBand { rate: 0.075, threshold: 400001.0 },
StampDutyBand { rate: 0.10, threshold: 750001.0 },
StampDutyBand { rate: 0.12, threshold: 1500001.0 },
],
annual_purchase_probability: 1.0,
}
}

#[test]
fn property_tax_routes_to_lbtt_in_scotland() {
let mut hh = Household::default();
hh.main_residence_value = 500_000.0;
hh.region = Region::Scotland;
// LBTT on £500k:
// 0% on first £145k = £0
// 2% on £145k-£250k (£105k) = £2,100
// 5% on £250k-£325k (£75k) = £3,750
// 10% on £325k-£500k (£175k) = £17,500
// total = £23,350
let tax = calculate_property_transaction_tax(
&hh, Some(&make_sdlt()), Some(&make_lbtt()), Some(&make_ltt())
);
assert!((tax - 23_350.0).abs() < 1.0, "got {}", tax);
}

#[test]
fn property_tax_routes_to_ltt_in_wales() {
let mut hh = Household::default();
hh.main_residence_value = 500_000.0;
hh.region = Region::Wales;
// LTT on £500k:
// 0% on first £180k = £0
// 3.5% on £180k-£250k (£70k) = £2,450
// 5% on £250k-£400k (£150k) = £7,500
// 7.5% on £400k-£500k (£100k) = £7,500
// total = £17,450
let tax = calculate_property_transaction_tax(
&hh, Some(&make_sdlt()), Some(&make_lbtt()), Some(&make_ltt())
);
assert!((tax - 17_450.0).abs() < 1.0, "got {}", tax);
}

#[test]
fn property_tax_routes_to_sdlt_outside_scotland_and_wales() {
let mut hh = Household::default();
hh.main_residence_value = 500_000.0;
hh.region = Region::London;
// Same as the existing stamp_duty_marginal test: £15,000.
let tax = calculate_property_transaction_tax(
&hh, Some(&make_sdlt()), Some(&make_lbtt()), Some(&make_ltt())
);
assert!((tax - 15_000.0).abs() < 1.0, "got {}", tax);
}

#[test]
fn property_tax_returns_zero_when_devolved_params_missing() {
let mut hh = Household::default();
hh.main_residence_value = 500_000.0;
hh.region = Region::Scotland;
// No LBTT params loaded → tax is 0 (regime doesn't fall back to SDLT).
let tax = calculate_property_transaction_tax(&hh, Some(&make_sdlt()), None, None);
assert_eq!(tax, 0.0);
}

#[test]
fn lbtt_zero_below_nil_band() {
let mut hh = Household::default();
hh.main_residence_value = 100_000.0; // below £145k LBTT nil-band ceiling
hh.region = Region::Scotland;
let tax = calculate_property_transaction_tax(
&hh, Some(&make_sdlt()), Some(&make_lbtt()), Some(&make_ltt())
);
assert_eq!(tax, 0.0);
}

#[test]
fn ltt_zero_below_nil_band() {
let mut hh = Household::default();
hh.main_residence_value = 150_000.0; // below £180k LTT nil-band ceiling
hh.region = Region::Wales;
let tax = calculate_property_transaction_tax(
&hh, Some(&make_sdlt()), Some(&make_lbtt()), Some(&make_ltt())
);
assert_eq!(tax, 0.0);
}

#[test]
fn wealth_tax_disabled() {
let params = WealthTaxParams { enabled: false, threshold: 10_000_000.0, rate: 0.01 };
Expand Down
88 changes: 88 additions & 0 deletions tests/policy/property_transaction_tax.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Property-transaction tax bands by region:
# - England + NI → SDLT (Finance Act 2003 s.55)
# - Scotland → LBTT (LBTT (Scotland) Act 2013)
# - Wales → LTT (Land Transaction Tax (Wales) Act 2017)
#
# All amounts below are annualised (×0.043 ≈ 1/23-yr average holding period).
# Numbers verified against the engine on 2026-05-01 at year=2025.

- name: SDLT in London on a £500k property
period: 2025
absolute_error_margin: 1
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: London, main_residence_value: 500_000 }
output:
# SDLT one-off: 0 + 2%×125k + 5%×250k = £15,000; annualised = £645.00
baseline_property_transaction_tax: 645

- name: LBTT in Scotland on a £500k property
period: 2025
absolute_error_margin: 2
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: Scotland, main_residence_value: 500_000 }
output:
# LBTT one-off: 2%×105k + 5%×75k + 10%×175k = £23,350; annualised = £1004.05
baseline_property_transaction_tax: 1004

- name: LTT in Wales on a £500k property
period: 2025
absolute_error_margin: 1
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: Wales, main_residence_value: 500_000 }
output:
# LTT one-off: 3.5%×70k + 5%×150k + 7.5%×100k = £17,450; annualised = £750.35
baseline_property_transaction_tax: 750

- name: Below LBTT nil-band (£100k in Scotland) — no tax
period: 2025
absolute_error_margin: 0
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: Scotland, main_residence_value: 100_000 }
output:
baseline_property_transaction_tax: 0

- name: Below LTT nil-band (£150k in Wales) — no tax
period: 2025
absolute_error_margin: 0
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: Wales, main_residence_value: 150_000 }
output:
baseline_property_transaction_tax: 0

- name: No property — no tax (Scotland)
period: 2025
absolute_error_margin: 0
input:
people:
p: { age: 40, employment_income: 0 }
benunits:
b: { members: [p] }
households:
h: { members: [p], region: Scotland }
output:
baseline_property_transaction_tax: 0
Loading