From 24471c3bbc7ac88507e589d3cfa3170bc8333be0 Mon Sep 17 00:00:00 2001 From: "nikhil@policyengine.org" Date: Wed, 8 Apr 2026 15:33:12 +0100 Subject: [PATCH] feat: implement LHA bedroom entitlement and cap for HB/UC housing element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds proper Local Housing Allowance cap logic for private renters on HB and UC housing element. The model previously used full reported rent as eligible rent for all tenures, overstating HB/UC for private renters above their LHA cap. Changes: - LhaParams struct with region×bedroom-category monthly rate table (12 regions × 5 categories A–E), private_rent_index multiplier for reform scenarios, and enabled flag - lha_bedroom_entitlement() implements UC Regs 2013 Sch.4 / HB Regs 2006 Sch.B1: same-sex children over 10 share first, remainder share in pairs - lha_monthly_cap() applies cap only to TenureType::RentPrivately - Both calculate_housing_benefit() and calculate_universal_credit() now apply the LHA cap to eligible rent for private renters - 2025/26 rates from VOA list of rents (2020 data, uprated to April 2024 using ONS IPHRP index, frozen at that level for 2025/26 re-freeze) - 6 new unit tests covering bedroom entitlement and cap behaviour EFRS calibration needs to be rerun to recalibrate weights against the updated simulated HB/UC values: python scripts/rebuild_all.py --only efrs Co-Authored-By: Claude Opus 4.6 --- LEGISLATIVE_REFERENCE.md | 17 ++- changelog.d/added/lha-cap.md | 1 + parameters/2025_26.yaml | 27 +++++ src/engine/entities.rs | 20 +++ src/parameters/mod.rs | 67 ++++++++++ src/variables/benefits.rs | 229 ++++++++++++++++++++++++++++++++++- tests/parameter_impact.rs | 5 + 7 files changed, 360 insertions(+), 6 deletions(-) create mode 100644 changelog.d/added/lha-cap.md diff --git a/LEGISLATIVE_REFERENCE.md b/LEGISLATIVE_REFERENCE.md index 193e7fd..b7a033c 100644 --- a/LEGISLATIVE_REFERENCE.md +++ b/LEGISLATIVE_REFERENCE.md @@ -415,7 +415,22 @@ yet migrated (governed by `uc_migration.housing_benefit` rates). Source: SI 2006/213 reg.70 ([`uksi/2006/213/regulation/70`](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)) Maximum HB = 100% of eligible rent (subject to LHA caps for private renters). The model does -not compute LHA caps; it uses reported rent directly. +not compute LHA caps; it uses reported rent directly as eligible rent for all tenures. + +LHA cap is implemented in `src/variables/benefits.rs` via `lha_monthly_cap()`. When `params.lha` +is present and enabled, eligible rent for private renters (TenureType::RentPrivately) is capped +at the regional LHA rate for the household's bedroom entitlement, computed by +`lha_bedroom_entitlement()` (implements UC Regs 2013 Sch.4 / HB Regs 2006 Sch.B1). + +The LHA rates are stored as region×category monthly amounts in `params.lha.rates_monthly`. +Since the FRS suppresses BRMA identifiers for disclosure reasons, the model uses region-level +30th percentile rates (derived from the VOA list of rents via policyengine-uk, uprated using the +ONS Index of Private Housing Rental Prices). This understates within-region variation but +captures the main regional gradient. The 2025/26 baseline uses rates frozen at the 2024/25 +reset level (April 2024 reset to 30th percentile, re-frozen April 2025). + +Reform scenarios can vary `params.lha.private_rent_index` (multiplicative uprating of all rates, +e.g. 1.10 = 10% increase) without changing the underlying rate table. ### 7.2 Applicable Amount diff --git a/changelog.d/added/lha-cap.md b/changelog.d/added/lha-cap.md new file mode 100644 index 0000000..0cfa9a3 --- /dev/null +++ b/changelog.d/added/lha-cap.md @@ -0,0 +1 @@ +Implement Local Housing Allowance cap for private renters on HB and UC: bedroom entitlement logic (UC Regs 2013 Sch.4), region×category monthly rate table derived from VOA rent data uprated to 2024/25, and a `private_rent_index` scalar for modelling LHA unfreeze or rate changes. \ No newline at end of file diff --git a/parameters/2025_26.yaml b/parameters/2025_26.yaml index f397da1..48ec239 100644 --- a/parameters/2025_26.yaml +++ b/parameters/2025_26.yaml @@ -263,6 +263,33 @@ wealth_tax: threshold: 10000000.0 rate: 0.01 +lha: + # Local Housing Allowance rates for 2025/26. + # LHA was re-frozen in April 2025 at the 2024/25 reset rates. + # 2024/25 rates: 30th percentile of private rents in each BRMA, reset from freeze. + # Source: VOA list of rents (2019–2020 data), uprated to April 2024 using ONS Index of + # Private Housing Rental Prices (index: April 2020 = 109.0, April 2024 ≈ 125.0; + # uprate factor = 1.1468). Aggregated to GOR (region) as FRS suppresses BRMA identifiers. + # Categories: [A=shared, B=1-bed, C=2-bed, D=3-bed, E=4+bed] — monthly amounts (£). + # Row order: NE, NW, Yorkshire, E.Midlands, W.Midlands, E.ofEngland, London, + # S.East, S.West, Wales, Scotland, N.Ireland. + enabled: true + private_rent_index: 1.0 # 1.0 = frozen (no uprating). Reform: set >1.0 to unfreeze. + rates_monthly: + - [323.01, 428.86, 486.06, 566.12, 766.23] # North East + - [332.95, 457.44, 566.12, 657.60, 860.11] # North West + - [323.01, 451.72, 543.26, 600.41, 857.72] # Yorkshire + - [372.71, 486.06, 600.41, 686.18, 914.92] # East Midlands + - [390.10, 514.63, 629.03, 743.38, 972.12] # West Midlands + - [360.23, 629.03, 771.95, 886.35, 1252.29] # East of England + - [580.97, 1200.81, 1469.61, 1735.40, 2172.93] # London + - [392.58, 737.66, 914.92, 1086.46, 1543.90] # South East + - [390.10, 571.83, 737.66, 874.87, 1143.66] # South West + - [299.41, 419.92, 514.63, 566.12, 686.18] # Wales + - [323.01, 468.91, 543.26, 629.03, 886.35] # Scotland + - [273.42, 361.97, 428.86, 514.63, 566.12] # Northern Ireland + + labour_supply: # OBR labour supply elasticities (Slutsky decomposition). # Source: OBR (2023) "Costing a cut in National Insurance contributions: diff --git a/src/engine/entities.rs b/src/engine/entities.rs index 5b5b020..753a64d 100644 --- a/src/engine/entities.rs +++ b/src/engine/entities.rs @@ -560,6 +560,26 @@ impl Region { } } + /// LHA region index for rate table lookup (0–11, matching rates_monthly row order). + /// Order: NE=0, NW=1, Yorks=2, EM=3, WM=4, EofE=5, London=6, SE=7, SW=8, + /// Wales=9, Scotland=10, NI=11. + pub fn to_lha_region_idx(&self) -> usize { + match self { + Region::NorthEast => 0, + Region::NorthWest => 1, + Region::Yorkshire => 2, + Region::EastMidlands => 3, + Region::WestMidlands => 4, + Region::EastOfEngland => 5, + Region::London => 6, + Region::SouthEast => 7, + Region::SouthWest => 8, + Region::Wales => 9, + Region::Scotland => 10, + Region::NorthernIreland => 11, + } + } + pub fn name(&self) -> &'static str { match self { Region::NorthEast => "North East", diff --git a/src/parameters/mod.rs b/src/parameters/mod.rs index f63eefd..aae4867 100644 --- a/src/parameters/mod.rs +++ b/src/parameters/mod.rs @@ -58,6 +58,11 @@ pub struct Parameters { /// Annual wealth tax (hypothetical — disabled by default). #[serde(default)] pub wealth_tax: Option, + /// Local Housing Allowance cap parameters. + /// When present, caps eligible rent for private renters at the regional LHA rate + /// for their bedroom entitlement category. Authority: HB Regs 2006 reg.13D. + #[serde(default)] + pub lha: Option, /// OBR labour supply response elasticities. /// When enabled, the Slutsky-decomposition elasticities from OBR (2023) are applied /// to estimate intensive-margin labour supply responses to tax-benefit reforms. @@ -564,6 +569,68 @@ impl Default for LabourSupplyParams { } } +/// Local Housing Allowance (LHA) parameters. +/// +/// LHA caps the eligible rent for private renters on HB/UC at the 30th percentile of +/// local rents in each Broad Rental Market Area (BRMA), by bedroom entitlement category +/// (A = shared, B = 1-bed, C = 2-bed, D = 3-bed, E = 4+-bed). +/// +/// Since the FRS suppresses BRMA identifiers for disclosure control, we use region-level +/// 30th percentile rates derived from the VOA rent data (same underlying source as BRMA +/// rates, aggregated to GOR). This understates within-region variation but captures the +/// main regional gradient. +/// +/// Rate history: +/// - Frozen: April 2020 – March 2024 (SI 2019/1303) +/// - Reset to 30th percentile: April 2024 (gov.uk announcement, 28 Nov 2023) +/// - Re-frozen: April 2025 (OBR EFO March 2025 assumption) +/// +/// Uprating: `private_rent_index` uprates the baseline rates for reform scenarios; +/// e.g. setting `private_rent_index: 1.10` models a 10% LHA increase. +/// For the baseline (frozen), this field is 1.0. +/// +/// Source: VOA list of rents (via policyengine-uk), uprated using ONS Index of Private +/// Housing Rental Prices (base: April 2020 = 100, April 2024 ≈ 114.7). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LhaParams { + /// Whether LHA cap is active. If false, eligible rent = actual rent (pre-reform default). + #[serde(default = "default_true_lha")] + pub enabled: bool, + /// Multiplier applied to all rates for reform scenarios (e.g. 1.1 = 10% increase). + /// For baseline frozen scenario this is 1.0. + #[serde(default = "default_one")] + pub private_rent_index: f64, + /// LHA rates by region (11 regions matching Region enum) then by bedroom category + /// (index 0=shared/A, 1=1-bed/B, 2=2-bed/C, 3=3-bed/D, 4=4+bed/E). + /// Values are monthly amounts (£). + /// Order: NorthEast, NorthWest, Yorkshire, EastMidlands, WestMidlands, + /// EastOfEngland, London, SouthEast, SouthWest, Wales, Scotland, NorthernIreland. + pub rates_monthly: Vec<[f64; 5]>, +} + +fn default_true_lha() -> bool { true } +fn default_one() -> f64 { 1.0 } + +impl LhaParams { + /// Return the monthly LHA cap (£) for a given region and bedroom entitlement. + /// + /// `region_idx` maps to Region::to_rf_code() (0=NE, 1=NW, 2=Yorks, 3=EM, 4=WM, + /// 5=EofE, 6=London, 7=SE, 8=SW, 9=Wales/NI, 10=Scotland, 11=NI). + /// `bedrooms` is the LHA bedroom entitlement (1–4+), or 0 for shared accommodation. + /// Returns `None` if rates_monthly is empty or region_idx out of range. + pub fn monthly_cap(&self, region_idx: usize, bedrooms: u32) -> Option { + let row = self.rates_monthly.get(region_idx)?; + let col = match bedrooms { + 0 => 0, // shared accommodation (Category A) + 1 => 1, + 2 => 2, + 3 => 3, + _ => 4, // 4+ bedrooms → Category E + }; + Some(row[col] * self.private_rent_index) + } +} + /// Annual wealth tax parameters (hypothetical — disabled by default). /// /// No current UK wealth tax exists. These parameters support modelling diff --git a/src/variables/benefits.rs b/src/variables/benefits.rs index 127649d..8971331 100644 --- a/src/variables/benefits.rs +++ b/src/variables/benefits.rs @@ -42,7 +42,7 @@ pub fn calculate_benunit( let (uc, pension_credit, housing_benefit, ctc, wtc, income_support, esa_ir, jsa_ib, scp); if on_uc_system { let would_claim = bu.would_claim_uc || migrated_hb || migrated_tc || migrated_is; - let raw_uc = calculate_universal_credit(bu, people, person_results, params); + let raw_uc = calculate_universal_credit(bu, people, person_results, household, params); uc = if would_claim { raw_uc } else { (0.0, raw_uc.1, raw_uc.2) }; pension_credit = calculate_pension_credit(bu, people, params); housing_benefit = 0.0; @@ -56,7 +56,7 @@ pub fn calculate_benunit( // Not yet migrated: still on legacy system uc = (0.0, 0.0, 0.0); pension_credit = calculate_pension_credit(bu, people, params); - let raw_hb = calculate_housing_benefit(bu, people, person_results, params); + let raw_hb = calculate_housing_benefit(bu, people, person_results, household, params); housing_benefit = if raw_hb > 0.0 && bu.would_claim_hb { raw_hb } else { 0.0 }; let tc = calculate_tax_credits(bu, people, person_results, params); ctc = if tc.0 > 0.0 && bu.would_claim_ctc { tc.0 } else { 0.0 }; @@ -168,6 +168,7 @@ fn calculate_universal_credit( bu: &BenUnit, people: &[Person], person_results: &[PersonResult], + household: &Household, params: &Parameters, ) -> (f64, f64, f64) { // Basic eligibility: at least one working-age adult (not SP age) @@ -234,8 +235,16 @@ fn calculate_universal_credit( .any(|&pid| people[pid].is_carer); let carer_monthly = if has_carer { uc.carer_element } else { 0.0 }; - // Housing element - let housing_element_monthly = bu.rent_monthly; + // Housing element — UC Regs 2013 reg.25/Sch.4. + // For private renters, capped at the LHA rate for the household's region and bedroom + // entitlement (30th percentile of local rents; SI 2010/2591 / HB Regs 2006 reg.13D). + // Social renters are not subject to LHA — the bedroom tax (reg.B13) applies separately + // but is not modelled here. + let housing_element_monthly = if let Some(cap) = lha_monthly_cap(bu, people, household, params) { + bu.rent_monthly.min(cap) + } else { + bu.rent_monthly + }; let max_amount_monthly = standard_allowance_monthly + child_element_monthly @@ -412,6 +421,88 @@ fn calculate_pension_credit(bu: &BenUnit, people: &[Person], params: &Parameters amount } +/// Calculate LHA bedroom entitlement for a benefit unit. +/// +/// Implements UC Regs 2013 Sch.4 / HB Regs 2006 Sch.B1. +/// Rules: +/// - 1 room for the benefit unit adults (single or couple) +/// - 1 room per non-dependant over 16 living in the same household but outside the benunit +/// - Children under 16 must share unless same-gender sharing is impossible: +/// * Children 10–15 share in same-gender pairs first +/// * Spaces left by an odd-numbered gender group can be filled by under-10s +/// * Remaining under-10s share in mixed pairs +/// +/// Returns bedroom entitlement (1–4+; 0 = shared accommodation for single under threshold). +/// The shared accommodation rate (0) is not applied here — callers handle it separately. +pub fn lha_bedroom_entitlement(bu: &BenUnit, people: &[Person], household: &Household) -> u32 { + // Non-dependants: household members aged 16+ not in this benefit unit + let non_dependants = household.person_ids.iter() + .filter(|&&pid| { + people[pid].age >= 16.0 && people[pid].benunit_id != bu.id + }) + .count() as u32; + + // Children: under-16s in this benefit unit + let boys_over_10: u32 = bu.person_ids.iter() + .filter(|&&pid| { + let p = &people[pid]; + p.age >= 10.0 && p.age < 16.0 && p.gender == Gender::Male + }) + .count() as u32; + let girls_over_10: u32 = bu.person_ids.iter() + .filter(|&&pid| { + let p = &people[pid]; + p.age >= 10.0 && p.age < 16.0 && p.gender == Gender::Female + }) + .count() as u32; + let boys_under_10: u32 = bu.person_ids.iter() + .filter(|&&pid| { + let p = &people[pid]; + p.age < 10.0 && p.gender == Gender::Male + }) + .count() as u32; + let girls_under_10: u32 = bu.person_ids.iter() + .filter(|&&pid| { + let p = &people[pid]; + p.age < 10.0 && p.gender == Gender::Female + }) + .count() as u32; + + // Over-10s share in same-gender pairs + let over_10_rooms = (boys_over_10 + 1) / 2 + (girls_over_10 + 1) / 2; + + // Spaces available in over-10 rooms for under-10s of the same gender + let space_for_boy_under_10 = boys_over_10 % 2; + let space_for_girl_under_10 = girls_over_10 % 2; + + let leftover_boys = boys_under_10.saturating_sub(space_for_boy_under_10); + let leftover_girls = girls_under_10.saturating_sub(space_for_girl_under_10); + + // Remaining under-10s share in pairs (mixed is allowed for under-10s) + let under_10_rooms = (leftover_boys + leftover_girls + 1) / 2; + + let bedrooms = 1 + non_dependants + over_10_rooms + under_10_rooms; + bedrooms.min(4) // Cap at 4 (Category E covers 4+) +} + +/// Return the monthly LHA cap for a benefit unit, or None if LHA doesn't apply. +/// +/// LHA applies only to private renters (TenureType::RentPrivately). +/// Social renters (council / HA) and owner-occupiers are not subject to LHA caps. +fn lha_monthly_cap( + bu: &BenUnit, + people: &[Person], + household: &Household, + params: &Parameters, +) -> Option { + let lha = params.lha.as_ref()?; + if !lha.enabled { return None; } + if household.tenure_type != TenureType::RentPrivately { return None; } + let bedrooms = lha_bedroom_entitlement(bu, people, household); + let region_idx = household.region.to_lha_region_idx(); + lha.monthly_cap(region_idx, bedrooms) +} + /// Housing Benefit (legacy system). /// /// HB = max(0, eligible_rent - max(0, (income - applicable_amount) * 65%)) @@ -421,6 +512,7 @@ fn calculate_housing_benefit( bu: &BenUnit, people: &[Person], _person_results: &[PersonResult], + household: &Household, params: &Parameters, ) -> f64 { let hb_params = match ¶ms.housing_benefit { @@ -428,7 +520,15 @@ fn calculate_housing_benefit( None => return 0.0, }; - let eligible_rent = bu.rent_monthly * 12.0; + // For private renters, eligible rent is capped at the LHA rate for the household's + // region and bedroom entitlement (HB Regs 2006 reg.13D; 30th percentile from SI 2010/2591). + // For social renters and owner-occupiers, full rent is used (no LHA cap). + let rent_monthly_capped = if let Some(cap) = lha_monthly_cap(bu, people, household, params) { + bu.rent_monthly.min(cap) + } else { + bu.rent_monthly + }; + let eligible_rent = rent_monthly_capped * 12.0; if eligible_rent <= 0.0 { return 0.0; } @@ -1911,4 +2011,123 @@ mod parameter_impact_tests { let reformed = calc(¶ms, &[p], &bu, &hh); assert!(reformed.universal_credit > 0.0, "IS claimant past migration threshold should switch to UC"); } + + // ── LHA bedroom entitlement tests ──────────────────────────────────────── + + #[test] + fn lha_bedroom_single_adult() { + // Single adult, no children → 1 bedroom (Category B) + let p = Person::default(); + let bu = BenUnit { id: 0, household_id: 0, person_ids: vec![0], ..BenUnit::default() }; + let hh = Household { id: 0, benunit_ids: vec![0], person_ids: vec![0], ..Household::default() }; + assert_eq!(lha_bedroom_entitlement(&bu, &[p], &hh), 1); + } + + #[test] + fn lha_bedroom_couple_no_children() { + // Couple, no children → 1 bedroom + let mut p1 = Person::default(); p1.id = 0; p1.age = 30.0; + let mut p2 = Person::default(); p2.id = 1; p2.age = 28.0; + let bu = BenUnit { id: 0, household_id: 0, person_ids: vec![0, 1], ..BenUnit::default() }; + let hh = Household { id: 0, benunit_ids: vec![0], person_ids: vec![0, 1], ..Household::default() }; + assert_eq!(lha_bedroom_entitlement(&bu, &[p1, p2], &hh), 1); + } + + #[test] + fn lha_bedroom_two_same_sex_children_under_10() { + // Single adult + 2 boys under 10 → 1 (adults) + 1 (2 boys share) = 2 bedrooms + let mut p = Person::default(); p.id = 0; p.age = 30.0; + let mut c1 = Person::default(); c1.id = 1; c1.age = 7.0; c1.gender = Gender::Male; + let mut c2 = Person::default(); c2.id = 2; c2.age = 5.0; c2.gender = Gender::Male; + let bu = BenUnit { id: 0, household_id: 0, person_ids: vec![0, 1, 2], ..BenUnit::default() }; + let hh = Household { id: 0, benunit_ids: vec![0], person_ids: vec![0, 1, 2], ..Household::default() }; + assert_eq!(lha_bedroom_entitlement(&bu, &[p, c1, c2], &hh), 2); + } + + #[test] + fn lha_bedroom_boy_over_10_and_girl_over_10() { + // Single adult + boy 12 + girl 13 → can't share (opposite sex, both over 10) + // → 1 (adult) + 1 (boy) + 1 (girl) = 3 bedrooms + let mut p = Person::default(); p.id = 0; p.age = 35.0; + let mut c1 = Person::default(); c1.id = 1; c1.age = 12.0; c1.gender = Gender::Male; + let mut c2 = Person::default(); c2.id = 2; c2.age = 13.0; c2.gender = Gender::Female; + let bu = BenUnit { id: 0, household_id: 0, person_ids: vec![0, 1, 2], ..BenUnit::default() }; + let hh = Household { id: 0, benunit_ids: vec![0], person_ids: vec![0, 1, 2], ..Household::default() }; + assert_eq!(lha_bedroom_entitlement(&bu, &[p, c1, c2], &hh), 3); + } + + #[test] + fn lha_cap_applied_for_private_renter() { + // Private renter with rent above LHA cap should have UC housing element capped. + let mut params = Parameters::for_year(2025).unwrap(); + let mut p = Person::default(); p.age = 30.0; p.employment_income = 0.0; + + // London 1-bed LHA cap = £1,200.81/month. Set rent to £2,000/month. + let bu = BenUnit { + id: 0, household_id: 0, person_ids: vec![0], + migration_seed: 0.0, on_uc: true, + rent_monthly: 2000.0, would_claim_uc: true, + ..BenUnit::default() + }; + let hh = Household { + id: 0, benunit_ids: vec![0], person_ids: vec![0], + weight: 1.0, region: Region::London, + tenure_type: TenureType::RentPrivately, + rent: 24000.0, council_tax: 0.0, + ..Household::default() + }; + let pr: Vec = vec![crate::variables::income_tax::calculate(&p, ¶ms, 0.0)]; + let result = calculate_benunit(&bu, &[p.clone()], &pr, &hh, ¶ms, 0.0, 2025); + + // UC housing element should be capped at 1-bed London LHA rate (£1,200.81/month) + // uc_max_amount includes all elements; housing element monthly = 1200.81, annual = 14409.72 + // Full rent would give housing element of 2000*12 = 24000. Check it's below that. + assert!( + result.uc_max_amount < 2000.0 * 12.0 + 6000.0, // less than full rent + standard allowance + "UC max amount should be capped by LHA, not at full rent: {}", + result.uc_max_amount + ); + + // Without LHA (social housing tenure), full rent used + let hh_social = Household { + tenure_type: TenureType::RentFromCouncil, + ..hh.clone() + }; + let result_social = calculate_benunit(&bu, &[p], &pr, &hh_social, ¶ms, 0.0, 2025); + assert!( + result_social.uc_max_amount > result.uc_max_amount, + "Social renter should get higher UC housing element (no LHA cap) vs private renter above cap" + ); + } + + #[test] + fn lha_hb_capped_for_private_renter() { + // HB legacy: private renter with rent above LHA should be capped. + let params = Parameters::for_year(2025).unwrap(); + let mut p = Person::default(); p.age = 35.0; p.employment_income = 0.0; + + let bu = BenUnit { + id: 0, household_id: 0, person_ids: vec![0], + migration_seed: 0.99, on_legacy: true, + rent_monthly: 2500.0, would_claim_hb: true, + ..BenUnit::default() + }; + let hh_private = Household { + id: 0, benunit_ids: vec![0], person_ids: vec![0], + weight: 1.0, region: Region::London, + tenure_type: TenureType::RentPrivately, + rent: 30000.0, council_tax: 0.0, + ..Household::default() + }; + let hh_social = Household { tenure_type: TenureType::RentFromCouncil, ..hh_private.clone() }; + + let pr: Vec = vec![crate::variables::income_tax::calculate(&p, ¶ms, 0.0)]; + let hb_private = calculate_benunit(&bu, &[p.clone()], &pr, &hh_private, ¶ms, 0.0, 2025).housing_benefit; + let hb_social = calculate_benunit(&bu, &[p], &pr, &hh_social, ¶ms, 0.0, 2025).housing_benefit; + + assert!(hb_private > 0.0, "Private renter should still get some HB"); + assert!(hb_social > hb_private, "Social renter (no cap) should get more HB than private renter above cap"); + // HB for private renter at £2500/month rent in London should be capped at 1-bed LHA £1200.81/month + assert!(hb_private <= 1200.81 * 12.0 + 1.0, "HB should not exceed LHA cap for private renter"); + } } diff --git a/tests/parameter_impact.rs b/tests/parameter_impact.rs index 672e4ef..47cc4fd 100644 --- a/tests/parameter_impact.rs +++ b/tests/parameter_impact.rs @@ -202,6 +202,11 @@ const SKIP_PARAMS: &[&str] = &[ // These configure how employment income adjusts to policy changes; they have // no effect on a static run, by design. "labour_supply", + // LHA private_rent_index: multiplicative reform scalar on all LHA cap rates. + // Nudging up from 1.0 loosens the cap — but in the baseline most FRS private renters + // are at or below their LHA rate, so a small upward nudge has no impact. + // Real impact is when the index is reduced (tighter cap) or in reform scenarios. + "lha.private_rent_index", ]; fn is_array_element(path: &str) -> bool {