diff --git a/changelog.d/added/lbtt-ltt.md b/changelog.d/added/lbtt-ltt.md new file mode 100644 index 0000000..8d7685e --- /dev/null +++ b/changelog.d/added/lbtt-ltt.md @@ -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. diff --git a/interfaces/python/policyengine_uk_compiled/models.py b/interfaces/python/policyengine_uk_compiled/models.py index 79e44ac..0882553 100644 --- a/interfaces/python/policyengine_uk_compiled/models.py +++ b/interfaces/python/policyengine_uk_compiled/models.py @@ -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 diff --git a/parameters/2025_26.yaml b/parameters/2025_26.yaml index 48ec239..41df0d5 100644 --- a/parameters/2025_26.yaml +++ b/parameters/2025_26.yaml @@ -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 } @@ -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. diff --git a/src/data/clean.rs b/src/data/clean.rs index c55321c..2d64bf0 100644 --- a/src/data/clean.rs +++ b/src/data/clean.rs @@ -553,10 +553,12 @@ fn write_microdata_csv_households( // ── 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", ])?; @@ -576,6 +578,7 @@ fn write_microdata_csv_households( 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 @@ -583,6 +586,7 @@ fn write_microdata_csv_households( 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), ])?; diff --git a/src/engine/simulation.rs b/src/engine/simulation.rs index 32a71f6..6b407d2 100644 --- a/src/engine/simulation.rs +++ b/src/engine/simulation.rs @@ -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() diff --git a/src/parameters/mod.rs b/src/parameters/mod.rs index aae4867..ad69075 100644 --- a/src/parameters/mod.rs +++ b/src/parameters/mod.rs @@ -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, - /// 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, + /// 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, + /// 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, /// Annual wealth tax (hypothetical — disabled by default). #[serde(default)] pub wealth_tax: Option, diff --git a/src/variables/wealth_taxes.rs b/src/variables/wealth_taxes.rs index 8203c3e..866c2d9 100644 --- a/src/variables/wealth_taxes.rs +++ b/src/variables/wealth_taxes.rs @@ -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. @@ -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. @@ -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 }; diff --git a/tests/policy/property_transaction_tax.yaml b/tests/policy/property_transaction_tax.yaml new file mode 100644 index 0000000..af00926 --- /dev/null +++ b/tests/policy/property_transaction_tax.yaml @@ -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