From 69b2703d7b5dc1cb185f18770d050c4f51a3565b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 16:02:07 -0400 Subject: [PATCH 01/10] Charge in-unit non-dependants in the local Council Tax Reduction schemes The five English working-age local schemes (Merton, Kingston upon Thames, Newham, Westminster, Oxford) reproduce the Default Scheme's para 9: an adult in the applicant's benefit unit who is not the claimant, partner, child or young person is a non-dependant. local_non_dep_deductions now charges the claiming family the household pool (claims x pool x share, as the national variable), counting the higher of each non-dependant couple (each member in Merton and Kingston where the couple has UC) and every other adult separately. normal_gross_income_non_dep_deduction bands a claimant/partner on the couple's joint income and anyone else on their own, uses each person's own hours, and gates the IS/JSA/ESA/PC/UC exemptions to the award's claimant and partner. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2014-local.fixed.md | 1 + .../local_scheme_non_dependants.yaml | 675 ++++++++++++++++++ .../council_tax_reduction/_legacy.py | 79 +- 3 files changed, 726 insertions(+), 29 deletions(-) create mode 100644 changelog.d/2014-local.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml diff --git a/changelog.d/2014-local.fixed.md b/changelog.d/2014-local.fixed.md new file mode 100644 index 0000000000..129c493722 --- /dev/null +++ b/changelog.d/2014-local.fixed.md @@ -0,0 +1 @@ +Charge non-dependant deductions in the English working-age local Council Tax Reduction schemes (Merton, Kingston upon Thames, Newham, Westminster, Oxford) for adults in the applicant's own benefit unit, deduct separately for each adult in another family who is not its claimant or partner, band each on their own income and hours, and apply benefit-receipt exemptions only to the award's claimant and partner. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml new file mode 100644 index 0000000000..fbefab0fd2 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -0,0 +1,675 @@ +# Non-dependant deductions in the English working-age local schemes. +# +# Each council's scheme reproduces the Default Scheme's definition: a +# non-dependant is anyone who normally resides with the applicant other than +# a member of the applicant's family (a partner, or a child or qualifying young +# person the applicant is responsible for), a joint occupier, a commercial +# lodger or a paid carer (para 9; Oxford paras 42 and Annex 1). So an adult in +# the applicant's own benefit unit who is not the claimant, partner, child or +# young person is a non-dependant, and so is each adult in another family. +# Each non-dependant aged 18 or over is banded on their own normal gross weekly +# income and is in remunerative work on their own hours (para 30(1)-(2) and +# para 10); a couple's income is joint and the couple pays one deduction, the +# higher (para 30(3)-(4); Oxford para 44), except that Merton and Kingston +# upon Thames deduct for each member of a couple with a Universal Credit +# award. The benefit-receipt exemptions (para 30(8)) apply to the person on +# the award: a family's award is its claimant's and partner's. +# +# Weekly amounts for 2026-27 (52 weeks a year): +# - Merton (para 30) and Westminster (Appendix A), Oxford (para 43): £5.20 not +# in remunerative work or under £279; £10.60 from £279; £13.30 from £485; +# £15.95 from £605. +# - Kingston upon Thames (para 30A): £5.64; £11.50 from £279; £14.43 from +# £485; £17.31 from £605. +# - Newham (para 30B): £7.54; £14.96 from £183; £18.84 from £316; £22.61 from +# £394. +# Earnings of £30,000 are £576.92 a week, £20,000 are £384.62 and £12,000 are +# £230.77. The claimant has no income and no capital, so Merton, Kingston +# upon Thames, Westminster and Oxford support 100% of the £1,800 council tax +# and Newham 70% (£1,260), less the deductions. +# +# The claimant and partner flags are set because the model's inference treats +# an adult child as a lone parent's partner. + +- name: Merton charges the applicant's own non-dependant son for his own earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + # £13.30 a week on £576.92. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 691.60] + merton_council_tax_reduction_non_dep_deductions: 691.60 + council_tax_reduction: 1_108.40 + +- name: Merton deducts separately for a lone parent and her working son in another family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # The mother is not in remunerative work (£5.20) and is not banded on + # her son's earnings; the son pays £13.30 on his own. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 691.60] + merton_council_tax_reduction_non_dep_deductions: [962, 0] + council_tax_reduction: 838 + +- name: Merton exempts a non-dependant on Income Support but not the adult son in her benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + income_support: 1 + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30(8)(a): the Income Support is the mother's, not her son's. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 691.60] + council_tax_reduction: 1_108.40 + +- name: Merton exempts a Universal Credit non-dependant without earnings but not her earning son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + universal_credit: 1_000 + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30(8)(c): the mother's award is calculated on no earned income, as + # her son's earnings are not hers; he is not on her award. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 691.60] + merton_council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: Merton deducts for each member of a Universal Credit couple on their own hours + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + worker: + age: 35 + is_claimant_or_partner: true + employment_income: 30_000 + weekly_hours: 37.5 + partner: + age: 35 + is_claimant_or_partner: true + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + couple: + members: [worker, partner] + universal_credit: 1_000 + households: + household: + members: [claimant, worker, partner] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30(3): no single deduction where there is a UC award. The worker + # pays £13.30 on the couple's joint £576.92; the partner, not in + # remunerative work, £5.20. The joint award has earned income, so + # neither is exempt under para 30(8)(c). + merton_council_tax_reduction_individual_non_dep_deduction: [0, 691.60, 270.40] + merton_council_tax_reduction_non_dep_deductions: [962, 0] + council_tax_reduction: 838 + +- name: Merton makes one deduction for a couple not on Universal Credit, on joint income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + first: + age: 35 + is_claimant_or_partner: true + employment_income: 15_000 + weekly_hours: 37.5 + second: + age: 35 + is_claimant_or_partner: true + employment_income: 15_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + couple: + members: [first, second] + would_claim_uc: false + households: + household: + members: [claimant, first, second] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30(4): £288.46 each, £576.92 jointly: £13.30, deducted once. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 691.60, 691.60] + merton_council_tax_reduction_non_dep_deductions: [691.60, 0] + council_tax_reduction: 1_108.40 + +- name: Merton makes no deduction for the son of an applicant who receives PIP daily living + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + pip_dl: 1_000 + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30(6)(b)(iii). + merton_council_tax_reduction_non_dep_deductions: 0 + council_tax_reduction: 1_800 + +- name: Kingston upon Thames charges the applicant's own non-dependant son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + council_tax: 1_800 + savings: 0 + output: + # Para 30A: £14.43 a week on £576.92. + kingston_upon_thames_council_tax_reduction_non_dep_deductions: 750.36 + council_tax_reduction: 1_049.64 + +- name: Kingston upon Thames deducts separately for a lone parent and her working son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 20_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + council_tax: 1_800 + savings: 0 + output: + # Mother £5.64; son £11.50 on £384.62. + kingston_upon_thames_council_tax_reduction_individual_non_dep_deduction: [0, 293.28, 598] + council_tax_reduction: 908.72 + +- name: Newham charges the applicant's own non-dependant son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: NEWHAM + council_tax: 1_800 + savings: 0 + output: + # Para 30B: £22.61 a week on £576.92; 70% of £1,800 is £1,260. + newham_council_tax_reduction_non_dep_deductions: 1_175.72 + council_tax_reduction: 84.28 + +- name: Newham deducts separately for a lone parent and her working son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 12_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: NEWHAM + council_tax: 1_800 + savings: 0 + output: + # Mother £7.54; son £14.96 on £230.77. + newham_council_tax_reduction_individual_non_dep_deduction: [0, 392.08, 777.92] + council_tax_reduction: 90 + +- name: Newham makes one deduction for a Universal Credit couple, the working member's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + worker: + age: 35 + is_claimant_or_partner: true + employment_income: 30_000 + weekly_hours: 37.5 + partner: + age: 35 + is_claimant_or_partner: true + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + couple: + members: [worker, partner] + universal_credit: 1_000 + households: + household: + members: [claimant, worker, partner] + country: ENGLAND + local_authority: NEWHAM + council_tax: 1_800 + savings: 0 + output: + # Para 30B(3) has no Universal Credit exception: the higher of £22.61 + # and £7.54. + newham_council_tax_reduction_individual_non_dep_deduction: [0, 1_175.72, 392.08] + newham_council_tax_reduction_non_dep_deductions: [1_175.72, 0] + council_tax_reduction: 84.28 + +- name: Westminster charges the applicant's own non-dependant son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: WESTMINSTER + council_tax: 1_800 + savings: 0 + output: + westminster_council_tax_reduction_non_dep_deductions: 691.60 + council_tax_reduction: 1_108.40 + +- name: Westminster makes no deduction for a full-time student son in the applicant's benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + in_HE: true + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: WESTMINSTER + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + westminster_council_tax_reduction_non_dep_deductions: 0 + council_tax_reduction: 1_800 + +- name: Westminster exempts a non-dependant on income-based JSA but not the adult son in her benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + jsa_income: 1 + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: WESTMINSTER + council_tax: 1_800 + savings: 0 + output: + westminster_council_tax_reduction_individual_non_dep_deduction: [0, 0, 691.60] + council_tax_reduction: 1_108.40 + +- name: Oxford charges the applicant's own non-dependant son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: OXFORD + council_tax: 1_800 + savings: 0 + output: + # Para 43: £13.30 a week on £576.92; the claimant's own income band is + # the 100% band. + oxford_council_tax_reduction_non_dep_deductions: 691.60 + council_tax_reduction: 1_108.40 + +- name: Oxford deducts separately for a lone parent and her working son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 20_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: OXFORD + council_tax: 1_800 + savings: 0 + output: + # Mother £5.20; son £10.60 on £384.62. + oxford_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 551.20] + council_tax_reduction: 978.40 + +- name: Oxford keeps deducting for a working-age non-dependant on Income Support + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + mother: + age: 40 + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + other_family: + members: [mother, son] + would_claim_uc: false + income_support: 1 + households: + household: + members: [claimant, mother, son] + country: ENGLAND + local_authority: OXFORD + council_tax: 1_800 + savings: 0 + output: + # Para 47: the benefit-receipt exemptions are for pensioner applicants + # only. + oxford_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 691.60] + council_tax_reduction: 838 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index fd35957f53..300cc583e1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -11,8 +11,7 @@ def in_other_non_liable_family(person, period): liable or paying the claimant. It is a proxy: a family jointly liable for the council tax but paying no rent is wrongly included, which #2009's claimant-family test fixes. The eligibility for a non-dependant deduction - and the local schemes' aggregation both use this, so they stay on one - definition.""" + uses this, for the national and local schemes alike.""" return ~person.benunit("benunit_contains_household_head", period) & ~person.benunit( "benunit_is_rent_liable", period ) @@ -88,31 +87,41 @@ def local_non_dep_deductions( individual_deduction_variable, one_deduction_for_uc_couples=True, ): - # The local schemes still charge only the non-dependants of other - # families. Non-dependants within a family's own benefit unit, now - # eligible for the national schemes, are left out here until each - # council's aggregation is brought into line. + # Every eligible person in the household is a non-dependant of each + # claiming family: the claimant's own family has only its benefit-unit + # non-dependants, and jointly liable sharers are not non-dependants (each + # scheme's para 9, the Default Scheme's wording). person = benunit.members - deductions = person(individual_deduction_variable, period) * ( - in_other_non_liable_family(person, period) - ) - deduction_for_benunit = benunit.max(deductions) + deductions = person(individual_deduction_variable, period) + # Only one deduction for a couple, the higher (para 30(3) of each scheme; + # Oxford para 44), except that Merton's and Kingston upon Thames's + # schemes deduct for each member of a couple with a Universal Credit + # award. Any other adult in the family is a non-dependant in their own + # right. + claimant_or_partner = person("is_claimant_or_partner", period) + higher_of_couple = ( + person.get_rank(person.benunit, -deductions, condition=claimant_or_partner) == 0 + ) + each_member_deducted = False if not one_deduction_for_uc_couples: - has_uc = benunit("universal_credit", period) > 0 - deduction_for_benunit = where( - has_uc, - benunit.sum(deductions), - deduction_for_benunit, - ) - is_benunit_head = benunit.members("is_benunit_head", period) - deductions_to_count = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max( - benunit.members.household.sum(deductions_to_count) + each_member_deducted = person.benunit("universal_credit", period) > 0 + counted_member = where( + claimant_or_partner, higher_of_couple | each_member_deducted, True + ) + counted = deductions * counted_member + # Members of the applicant's own family are not its non-dependants (para + # 9(2)(a)), as in council_tax_reduction_non_dep_deductions. + own_family_member = ~person( + "is_benefit_unit_non_dependant_for_legacy_benefits", period + ) + non_dependants = benunit.max(person.household.sum(counted)) - benunit.sum( + counted * own_family_member ) # A non-dependant of two or more jointly liable people is apportioned # equally between them (SI 2012/2885 Sch 1 para 8(5)). share = benunit("council_tax_reduction_joint_liability_share", period) - return (deductions_in_household - deduction_for_benunit) * share + claims = benunit("council_tax_reduction_claimant_benunit", period) + return claims * share * non_dependants def normal_gross_income_non_dep_deduction( @@ -138,29 +147,41 @@ def normal_gross_income_non_dep_deduction( ] gross_income = add(person, period, gross_income_components) earned_income = add(person, period, earned_income_components) - weekly_benunit_gross_income = person.benunit.sum(gross_income) / WEEKS_IN_YEAR - weekly_benunit_earned_income = person.benunit.sum(earned_income) / WEEKS_IN_YEAR - benunit_weekly_hours = person.benunit.max(person("weekly_hours", period)) + # A couple's joint income (para 30(4) of each scheme; Oxford para 44); + # another adult in the family is a non-dependant in their own right, on + # their own income. + claimant_or_partner = person("is_claimant_or_partner", period) + couple_gross_income = person.benunit.sum(gross_income * claimant_or_partner) + weekly_gross_income = ( + where(claimant_or_partner, couple_gross_income, gross_income) / WEEKS_IN_YEAR + ) + # Remunerative work is each person's own (para 10 and para 30(1)(a)). A + # couple paying one deduction pays the higher, so the working member's + # band on their joint income. in_remunerative_work = ( - benunit_weekly_hours >= ctr.non_dep_deduction.remunerative_work_hours + person("weekly_hours", period) >= ctr.non_dep_deduction.remunerative_work_hours ) weekly_deduction = where( in_remunerative_work, - ctr.non_dep_deduction.amount.calc(weekly_benunit_gross_income), + ctr.non_dep_deduction.amount.calc(weekly_gross_income), ctr.non_dep_deduction.amount.calc(0), ) claimant_exempt = person.household( "council_tax_reduction_household_has_non_dep_exemption", period ) full_time_student = is_full_time_student_non_dep(person, period) - income_based_benefit = ( + # Benefit-unit awards are the claimant's and partner's: another adult in + # the unit is not on them. + income_based_benefit = claimant_or_partner & ( (person.benunit("income_support", period) > 0) | (person.benunit("jsa_income", period) > 0) | (person.benunit("esa_income", period) > 0) | (person.benunit("pension_credit", period) > 0) ) - has_uc = person.benunit("universal_credit", period) > 0 - no_earned_income = weekly_benunit_earned_income <= 0 + has_uc = claimant_or_partner & (person.benunit("universal_credit", period) > 0) + # The award is the couple's, so it is calculated on their joint earned + # income. + no_earned_income = person.benunit.sum(earned_income * claimant_or_partner) <= 0 exempt = ( claimant_exempt | full_time_student From 570f163cea7be2de132171b984c7eff91632c9c0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 10:04:52 -0400 Subject: [PATCH 02/10] Test the local schemes' non-dependants against an oracle and each council's scale Hypothesis properties 6 and 7: each council's individual and family deductions equal an independent calculation from its 2026-27 scale, and an adult who is not a claimant or partner changes no one else's deduction through their earnings (both fail on the previous aggregation). A Merton case for a 19-year-old who has left education, and #2017's Newham case now apportions the head's own son between the jointly liable families (para 30(5)). Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 13 +- .../local_scheme_non_dependants.yaml | 35 ++ ...t_benefit_unit_non_dependant_properties.py | 346 ++++++++++++++++++ 3 files changed, 388 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index ffa7cbe70f..c1aec4c786 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -726,10 +726,11 @@ council_tax_reduction_individual_non_dep_deduction_eligible: [false, true, true] council_tax_reduction_non_dep_deductions: [300, 0] -# The English working-age local schemes keep their own aggregation, which -# charges only other families' non-dependants. Widening the shared -# eligibility must not make a jointly liable sharer bear the head's own son. -- name: Local CTR (Newham), a jointly liable sharer does not bear the head's own in-unit son +# The English working-age local schemes reproduce the Default Scheme's para 9 +# and para 30(5): the head's own son normally resides with both jointly liable +# people, so each bears half his deduction (Newham para 30B: £18.84 a week on +# £384.62, £979.68 a year). 70% of each half of the £1,800 is £630. +- name: Local CTR (Newham), jointly liable families share the deduction for the head's own in-unit son period: 2025 absolute_error_margin: 0.01 input: @@ -764,8 +765,8 @@ council_tax: 1_800 output: council_tax_reduction_joint_liability_share: [0.5, 0.5] - newham_council_tax_reduction_non_dep_deductions: [0, 0] - newham_council_tax_reduction: [630, 630] + newham_council_tax_reduction_non_dep_deductions: [489.84, 489.84] + newham_council_tax_reduction: [140.16, 140.16] # SI 2012/2885 reg 9(2)(a): the applicant's own family are not its # non-dependants, so an individual deduction set for the applicant (here as diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml index fbefab0fd2..492c43583c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -64,6 +64,41 @@ merton_council_tax_reduction_non_dep_deductions: 691.60 council_tax_reduction: 1_108.40 +- name: Merton charges a 19-year-old who has left education in the applicant's benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + daughter: + # Left education, so not a qualifying young person or a student (the + # education input otherwise defaults to tertiary at 19). + age: 19 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + employment_income: 12_000 + weekly_hours: 30 + benunits: + benunit: + members: [parent, daughter] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, daughter] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + is_benefit_unit_non_dependant_for_legacy_benefits: [false, true] + # In remunerative work on £230.77 a week, below £279: £5.20. + merton_council_tax_reduction_non_dep_deductions: 270.40 + council_tax_reduction: 1_529.60 + - name: Merton deducts separately for a lone parent and her working son in another family period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index 45a5ab4714..f39fd40735 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -27,6 +27,17 @@ 5. Bedrooms: every Universal Credit non-dependant within a benefit unit has a bedroom in the size criteria (para 10(1)(c)), so the deduction and the bedroom use one definition. +6. Local schemes: in Merton, Kingston upon Thames, Newham, Westminster and + Oxford, each non-dependant's deduction and each claiming family's total + equal an independent calculation from the council's scale: a couple banded + on joint earnings and deducted once at the higher amount (each member in + Merton and Kingston upon Thames when the couple has Universal Credit), any + other adult banded on their own earnings and hours, the benefit-receipt + exemptions only for a claimant or partner on the award (none in Oxford), + and the pool charged to each claiming family by its share, less its own + claimant, partner and children. Every other council's variables are zero. +7. Local independence: an adult in a benefit unit who is not its claimant or + partner changes no one else's local deduction through their earnings. """ import numpy as np @@ -426,3 +437,338 @@ def test_every_non_dependant_in_a_benefit_unit_has_a_bedroom(population): # A non-dependant has a bedroom as a responsible qualifying young person # would, so the count is the same either way. assert np.array_equal(rooms, rooms_as_dependants) + + +# 6-7. The English working-age local schemes. Each council's weekly scale for +# 2026-27 (the parameters' first values, which also apply in 2025): Merton +# para 30, Kingston upon Thames para 30A, Newham para 30B, Westminster +# Appendix A and Oxford para 43. +LOCAL_SCHEMES = { + "MERTON": dict( + name="merton", + thresholds=[0, 279, 485, 605], + amounts=[5.20, 10.60, 13.30, 15.95], + benefit_exemptions=True, + each_uc_couple_member=True, + ), + "KINGSTON_UPON_THAMES": dict( + name="kingston_upon_thames", + thresholds=[0, 279, 485, 605], + amounts=[5.64, 11.50, 14.43, 17.31], + benefit_exemptions=True, + each_uc_couple_member=True, + ), + "NEWHAM": dict( + name="newham", + thresholds=[0, 183, 316, 394], + amounts=[7.54, 14.96, 18.84, 22.61], + benefit_exemptions=True, + each_uc_couple_member=False, + ), + "WESTMINSTER": dict( + name="westminster", + thresholds=[0, 279, 485, 605], + amounts=[5.20, 10.60, 13.30, 15.95], + benefit_exemptions=True, + each_uc_couple_member=False, + ), + "OXFORD": dict( + name="oxford", + thresholds=[0, 279, 485, 605], + amounts=[5.20, 10.60, 13.30, 15.95], + benefit_exemptions=False, + each_uc_couple_member=False, + ), +} +EARNINGS = [0, 2_600, 15_600, 30_000] +HOURS = [0, 10, 37.5] + + +@st.composite +def local_family(draw, role): + # Working-age adults; the head's family has the household's oldest + # member, below State Pension age, and every extra member is at most 60. + adult_age = st.integers(61, 65) if role == "head" else st.integers(25, 59) + n = draw(st.integers(1, 2)) + + def adults(strategy): + return draw(st.lists(strategy, min_size=n, max_size=n)) + + return dict( + role=role, + adult_ages=adults(adult_age), + adult_earnings=adults(st.sampled_from(EARNINGS)), + adult_hours=adults(st.sampled_from(HOURS)), + extras=draw(st.lists(extra_member(), min_size=0, max_size=2)), + extra_students=draw(st.lists(st.booleans(), min_size=2, max_size=2)), + child=draw(st.booleans()), + renter_pip=draw(st.booleans()), + payment=draw(st.floats(1_000, 10_000, allow_nan=False)), + would_claim_uc=draw(st.booleans()), + ) + + +@st.composite +def local_household(draw): + head = draw(local_family("head")) + others = [ + draw(local_family(draw(st.sampled_from(ROLES)))) + for _ in range(draw(st.integers(0, 2))) + ] + return dict( + families=[head] + others, + rent=draw(st.floats(3_000, 20_000, allow_nan=False)), + tenure=draw(st.sampled_from(["RENT_FROM_COUNCIL", "RENT_PRIVATELY"])), + local_authority=draw(st.sampled_from(sorted(LOCAL_SCHEMES))), + ) + + +local_population = st.lists(local_household(), min_size=1, max_size=5) + + +def build_local(population, zero_extra_earnings=False): + """build(), plus each adult's earnings and hours, each person's education + (an extra member may be a full-time student), each family's Universal + Credit claim and each household's council in England.""" + situation, person_rows, family_rows = build(population) + people = list(situation["people"].values()) + row_iter = iter(zip(people, person_rows)) + for h, house in enumerate(population): + situation["households"][f"h{h}"].update( + country="ENGLAND", local_authority=house["local_authority"] + ) + for f, fam in enumerate(house["families"]): + situation["benunits"][f"h{h}_f{f}"]["would_claim_uc"] = fam[ + "would_claim_uc" + ] + members = ( + [("adult", i) for i in range(len(fam["adult_ages"]))] + + [("extra", i) for i in range(len(fam["extras"]))] + + ([("child", 0)] if fam["child"] else []) + ) + for kind, i in members: + person, row = next(row_iter) + if kind == "adult": + earnings = fam["adult_earnings"][i] + hours = fam["adult_hours"][i] + student = False + elif kind == "extra": + earnings = person["employment_income"] + if zero_extra_earnings: + earnings = 0.0 + hours = person["weekly_hours"] + student = fam["extra_students"][i] + else: + earnings, hours, student = 0.0, 0.0, False + person.update( + employment_income=float(earnings), + weekly_hours=float(hours), + current_education="TERTIARY" if student else "NOT_IN_EDUCATION", + ) + row.update( + earnings=float(earnings), + hours=float(hours), + student=student, + claimant_or_partner=kind == "adult", + local_authority=house["local_authority"], + ) + return situation, person_rows, family_rows + + +def weekly_scale(scheme, weekly_income): + amounts = [ + amount + for threshold, amount in zip(scheme["thresholds"], scheme["amounts"]) + if weekly_income >= threshold + ] + return amounts[-1] + + +def local_oracle(person_rows, family_rows, benefits, claims, ctr_share): + """Each person's deduction under their council's scheme and each family's + total, from the generated structure and the families' benefit awards.""" + n_people, n_families = len(person_rows), len(family_rows) + individual = np.zeros(n_people) + counted = np.zeros(n_people) + for p, row in enumerate(person_rows): + f = row["family"] + family = family_rows[f] + scheme = LOCAL_SCHEMES[row["local_authority"]] + # Reg 9: a member of a family not liable for the rent, or an adult in + # any family's benefit unit who is not its claimant, partner, child or + # young person; aged 18 or over. + eligible = row["age"] >= 18 and ( + family["role"] == "non_dependant" or row["kind"] == "non_dependant" + ) + head = next( + i + for i, other in enumerate(family_rows) + if other["household"] == family["household"] + ) + applicant_exempt = family_rows[head]["renter_pip"] + if not eligible or applicant_exempt or row["student"]: + continue + couple = [ + other + for other in person_rows + if other["family"] == f and other["claimant_or_partner"] + ] + if row["claimant_or_partner"]: + income = sum(member["earnings"] for member in couple) + else: + income = row["earnings"] + weekly = ( + weekly_scale(scheme, income / 52) + if row["hours"] >= 16 + else scheme["amounts"][0] + ) + exempt = False + if scheme["benefit_exemptions"] and row["claimant_or_partner"]: + award = benefits[f] + on_income_related = any( + award[b] > 0 + for b in ( + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + ) + ) + couple_earned = sum(member["earnings"] for member in couple) + exempt = on_income_related or ( + award["universal_credit"] > 0 and couple_earned <= 0 + ) + individual[p] = 0 if exempt else weekly * 52 + # One deduction for a couple, the higher, or each member's in Merton and + # Kingston upon Thames where the couple has Universal Credit; any other + # member separately. + for f, family in enumerate(family_rows): + members = [p for p, row in enumerate(person_rows) if row["family"] == f] + couple = [p for p in members if person_rows[p]["claimant_or_partner"]] + scheme = LOCAL_SCHEMES[person_rows[members[0]]["local_authority"]] + each = scheme["each_uc_couple_member"] and benefits[f]["universal_credit"] > 0 + for p in members: + if p not in couple or each: + counted[p] = individual[p] + if couple and not each: + counted[couple[0]] = max(individual[p] for p in couple) + totals = np.zeros(n_families) + for f, family in enumerate(family_rows): + in_household = [ + p + for p, row in enumerate(person_rows) + if family_rows[row["family"]]["household"] == family["household"] + ] + own = [ + p + for p in in_household + if person_rows[p]["family"] == f + and person_rows[p]["kind"] != "non_dependant" + ] + pool = sum(counted[p] for p in in_household) - sum(counted[p] for p in own) + totals[f] = claims[f] * ctr_share[f] * pool + return individual, totals + + +def run_local(population, zero_extra_earnings=False): + situation, person_rows, family_rows = build_local(population, zero_extra_earnings) + sim = Simulation(situation=situation) + awards = { + b: calc(sim, b) + for b in ( + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + "universal_credit", + ) + } + benefits = [{b: awards[b][f] for b in awards} for f in range(len(family_rows))] + claims = calc(sim, "council_tax_reduction_claimant_benunit") + ctr_share = calc(sim, "council_tax_reduction_joint_liability_share") + return sim, person_rows, family_rows, benefits, claims, ctr_share + + +@PROPERTY_SETTINGS +@given(local_population) +def test_local_scheme_deductions_match_the_oracle(population): + sim, person_rows, family_rows, benefits, claims, ctr_share = run_local(population) + individual, totals = local_oracle( + person_rows, family_rows, benefits, claims, ctr_share + ) + person_council = np.array([row["local_authority"] for row in person_rows]) + family_council = np.array( + [ + person_council[[r["family"] for r in person_rows].index(f)] + for f in range(len(family_rows)) + ] + ) + for council, scheme in LOCAL_SCHEMES.items(): + name = scheme["name"] + # 6. Each council's variables follow its own scheme and are zero + # elsewhere. + assert np.allclose( + calc(sim, f"{name}_council_tax_reduction_individual_non_dep_deduction"), + np.where(person_council == council, individual, 0), + atol=0.01, + ), name + assert np.allclose( + calc(sim, f"{name}_council_tax_reduction_non_dep_deductions"), + np.where(family_council == council, totals, 0), + atol=0.01, + ), name + + +def with_an_earning_adult_beside_its_claimant(population): + """Each household plus a family not liable for the rent whose claimant + works 37.5 hours without earnings beside a 22-year-old earning £30,000, + and an applicant with no exemption, so the property always binds.""" + family = dict( + role="non_dependant", + adult_ages=[40], + adult_earnings=[0], + adult_hours=[37.5], + extras=[ + dict( + kind="non_dependant", + age=22, + earnings=30_000, + hours=37.5, + attendance_allowance=False, + ) + ], + extra_students=[False, False], + child=False, + renter_pip=False, + payment=1_000.0, + would_claim_uc=False, + ) + return [ + dict( + house, + families=[dict(house["families"][0], renter_pip=False)] + + house["families"][1:] + + [family], + ) + for house in population + ] + + +@PROPERTY_SETTINGS +@given(local_population) +def test_local_scheme_in_unit_earnings_affect_only_their_own_deduction(population): + population = with_an_earning_adult_beside_its_claimant(population) + sim, person_rows, *_ = run_local(population) + without, *_ = run_local(population, zero_extra_earnings=True) + # 7. Zeroing the earnings of members who are not a claimant or partner + # leaves every claimant's and partner's deduction unchanged. + claimant_or_partner = np.array([row["claimant_or_partner"] for row in person_rows]) + for scheme in LOCAL_SCHEMES.values(): + variable = ( + f"{scheme['name']}_council_tax_reduction_individual_non_dep_deduction" + ) + assert np.allclose( + calc(sim, variable)[claimant_or_partner], + calc(without, variable)[claimant_or_partner], + atol=0.01, + ), variable From 663a5b7333c0a27492831ba7c7f33a463289dcf9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 07:34:18 -0400 Subject: [PATCH 03/10] Apply the para 9(2)(d)-(e) exclusions to the person, whatever their family Review r1 of #2077: an in-unit adult who personally pays the householder as a boarder or lodger, or is personally liable for the rent, was still a non-dependant, and a household head whom the inference left outside his benefit unit's couple was charged as his own non-dependant. The household head (the person liable for the council tax), anyone liable for the household's rent and anyone paying rent as a boarder or lodger is now nobody's non-dependant, in the national and local schemes alike. Property 6's oracle now derives who claims and each claim's share from the generated structure and checks the model's against them. Co-Authored-By: Claude Opus 5.5 --- .../local_scheme_non_dependants.yaml | 150 ++++++++++++++++++ ...t_benefit_unit_non_dependant_properties.py | 35 +++- ...n_individual_non_dep_deduction_eligible.py | 24 ++- 3 files changed, 203 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml index 492c43583c..72d41a926c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -708,3 +708,153 @@ # only. oxford_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 691.60] council_tax_reduction: 838 + +# Para 9(2)(d)-(e) exclude the person, whatever family they are in: someone who +# pays the householder as a boarder or lodger, someone jointly liable for the +# council tax, and the liable person (the household head) are nobody's +# non-dependants. + +- name: Merton makes no deduction for an adult son who himself pays as a lodger + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 40 + is_claimant_or_partner: true + rent_paid_as_lodger: 3_000 + son: + age: 25 + is_claimant_or_partner: false + rent_paid_as_lodger: 3_000 + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + applicant_family: + members: [applicant] + would_claim_uc: false + claims_all_entitled_benefits: true + lodging_family: + members: [lodger, son] + would_claim_uc: false + households: + household: + members: [applicant, lodger, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false] + merton_council_tax_reduction_non_dep_deductions: [0, 0] + council_tax_reduction: 1_800 + +- name: Merton charges a lodger's adult son who does not pay the householder himself + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 40 + is_claimant_or_partner: true + rent_paid_as_lodger: 3_000 + son: + age: 25 + is_claimant_or_partner: false + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + applicant_family: + members: [applicant] + would_claim_uc: false + claims_all_entitled_benefits: true + lodging_family: + members: [lodger, son] + would_claim_uc: false + households: + household: + members: [applicant, lodger, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 9(2)(e) excludes only the person liable to pay the applicant. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true] + merton_council_tax_reduction_non_dep_deductions: [691.60, 0] + council_tax_reduction: 1_108.40 + +- name: Merton makes no deduction for an adult son who is himself a joint tenant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + is_liable_for_household_rent: true + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 9(2)(d): jointly and severally liable with the applicant. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] + merton_council_tax_reduction_non_dep_deductions: 0 + +- name: Merton never charges the applicant as their own non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 45 + is_household_head: true + young_mother: + age: 17 + is_parent: true + young_father: + age: 17 + is_parent: true + baby: + age: 0 + benunits: + benunit: + members: [applicant, young_mother, young_father, baby] + would_claim_uc: false + households: + household: + members: [applicant, young_mother, young_father, baby] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # The inference makes the young parents the benefit unit's couple, so + # the household head is not its claimant or partner; he is still the + # person liable for the council tax, and everyone else is under 18. + is_claimant_or_partner: [false, true, true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false, false] + merton_council_tax_reduction_non_dep_deductions: 0 diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index f39fd40735..de7ddb1db0 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -684,15 +684,42 @@ def run_local(population, zero_extra_earnings=False): ) } benefits = [{b: awards[b][f] for b in awards} for f in range(len(family_rows))] - claims = calc(sim, "council_tax_reduction_claimant_benunit") - ctr_share = calc(sim, "council_tax_reduction_joint_liability_share") - return sim, person_rows, family_rows, benefits, claims, ctr_share + return sim, person_rows, family_rows, benefits + + +def legal_claims_and_shares(family_rows): + """Who claims and each claim's share, from the generated structure alone. + Where a family shares the rent, every family liable for it (the head's + and each sharer's) is jointly and severally liable for the council tax and + claims, each person liable bearing an equal share of the tax and of each + shared non-dependant's deduction (para 29(3)-(4), 30(5)); otherwise the + head's family claims alone, in full.""" + n = len(family_rows) + claims, shares = np.zeros(n), np.ones(n) + for h in {family["household"] for family in family_rows}: + families = [i for i, f in enumerate(family_rows) if f["household"] == h] + liable = [i for i in families if family_rows[i]["role"] in ("head", "sharer")] + shared = any(family_rows[i]["role"] == "sharer" for i in families) + liable_people = sum(family_rows[i]["n_liable"] for i in liable) + for i in families: + if shared: + claims[i] = i in liable + shares[i] = 1 / liable_people if i in liable else 1 + else: + claims[i] = family_rows[i]["role"] == "head" + return claims, shares @PROPERTY_SETTINGS @given(local_population) def test_local_scheme_deductions_match_the_oracle(population): - sim, person_rows, family_rows, benefits, claims, ctr_share = run_local(population) + sim, person_rows, family_rows, benefits = run_local(population) + claims, ctr_share = legal_claims_and_shares(family_rows) + # The claims and shares the totals use are the scheme's, not the model's. + assert np.array_equal(calc(sim, "council_tax_reduction_claimant_benunit"), claims) + assert np.allclose( + calc(sim, "council_tax_reduction_joint_liability_share"), ctr_share + ) individual, totals = local_oracle( person_rows, family_rows, benefits, claims, ctr_share ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 2c39ee3daa..4043364281 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -16,7 +16,11 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): "including a boarder's or lodger's adult son. Someone jointly and " "severally liable for the council tax with the claimant (a joint " "tenant or other sharer of the rent) and a boarder or lodger who pays " - "the claimant are not non-dependants." + "the claimant are not non-dependants. Those exclusions belong to the " + "person, whatever family they are in: the household head (the person " + "liable for the council tax), anyone liable for the household's rent " + "and anyone who pays rent as a boarder or lodger is nobody's " + "non-dependant." ) definition_period = YEAR reference = ( @@ -34,4 +38,20 @@ def formula(person, period, parameters): # family. Reg 9(2)(e) excludes only the person liable to pay the # applicant, so a boarder's or lodger's adult son is a non-dependant. in_unit = person("is_benefit_unit_non_dependant_for_legacy_benefits", period) - return (person("age", period) >= 18) & (other_family | in_unit) + # Reg 9(2)(d)-(e) exclude the person, not their family: the liable + # person (LGFA 1992 s.6) and any joint occupier are jointly and + # severally liable with an applicant, and a boarder or lodger pays on + # a commercial basis. So an applicant is never their own + # non-dependant, even where the benefit unit's claimant and partner + # are other members of it. + personally_excluded = ( + person("is_household_head", period) + | person("is_liable_for_household_rent", period) + | (person("rent_paid_as_lodger", period) > 0) + | (person("rent_paid_as_boarder", period) > 0) + ) + return ( + (person("age", period) >= 18) + & (other_family | in_unit) + & ~personally_excluded + ) From 682a98f8ad49338a51ba11e9b453a7bbe6c2807b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:04:27 -0400 Subject: [PATCH 04/10] Test the local Universal Credit exemption on each person's own earnings, and #2017's head-alone couple Para 30(8)(c) of each local scheme exempts a non-dependant entitled to Universal Credit "calculated on the basis that the person does not have any earned income". The merge reads it on the person's own earnings, as #2007's national schemes read the same words, so the non-earning member of a UC couple is exempt: Merton's couple pays 691.60, not 962 (CTR 1,108.40), and Newham's partner's own deduction is nil (the couple's one deduction, the worker's, is unchanged). Main's IIDB case for a grandmother who applies alone noted that the model charged nothing for the couple in her benefit unit until #2017. With #2017 they are her non-dependants: one deduction, the lowest (5.20 a week), so council_tax_benefit is 1,195.968. Co-Authored-By: Claude Opus 5.5 --- ...cil_tax_reduction_prescribed_benefits.yaml | 12 ++++++--- .../local_scheme_non_dependants.yaml | 27 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml index ea63047128..ee29ceeca5 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml @@ -570,10 +570,14 @@ # A single pensioner who reached pensionable age before April 2021: # 256.00 * 52 = 13,312.00. council_tax_reduction_applicable_amount: 13_312 - # 1,800 - 0.2 x (14,980.16 - 13,312.00) = 1,800 - 333.632 = 1,466.368. - # The couple are in her benefit unit, so the model charges no - # non-dependant deduction for them (#2017). - council_tax_benefit: 1_466.368 + # 1,800 - 0.2 x (14,980.16 - 13,312.00) = 1,800 - 333.632 = 1,466.368, + # less a non-dependant deduction: the couple are in her benefit unit but + # not her family, so are her non-dependants (SI 2012/2885 reg 9; #2017). + # They pay one deduction, the higher (Sch 1 para 8(3)); neither is in + # remunerative work, so each would pay the lowest amount, 5.20 a week + # (para 8(1)(b)): 5.20 x 52 = 270.40. 1,466.368 - 270.40 = 1,195.968. + council_tax_reduction_non_dep_deductions: 270.40 + council_tax_benefit: 1_195.968 - name: A guarantee credit recipient with IIDB has nil Council Tax Reduction income period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml index 72d41a926c..64686f3ab5 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -13,7 +13,9 @@ # higher (para 30(3)-(4); Oxford para 44), except that Merton and Kingston # upon Thames deduct for each member of a couple with a Universal Credit # award. The benefit-receipt exemptions (para 30(8)) apply to the person on -# the award: a family's award is its claimant's and partner's. +# the award: Income Support, income-based JSA, income-related ESA and State +# Pension Credit to the member they are payable to, and Universal Credit to +# each joint claimant who has no earned income of their own. # # Weekly amounts for 2026-27 (52 weeks a year): # - Merton (para 30) and Westminster (Appendix A), Oxford (para 43): £5.20 not @@ -214,7 +216,7 @@ merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 691.60] merton_council_tax_reduction_non_dep_deductions: [691.60, 0] -- name: Merton deducts for each member of a Universal Credit couple on their own hours +- name: Merton deducts for each member of a Universal Credit couple on their own hours and earnings period: 2026 absolute_error_margin: 0.01 input: @@ -248,12 +250,14 @@ savings: 0 output: # Para 30(3): no single deduction where there is a UC award. The worker - # pays £13.30 on the couple's joint £576.92; the partner, not in - # remunerative work, £5.20. The joint award has earned income, so - # neither is exempt under para 30(8)(c). - merton_council_tax_reduction_individual_non_dep_deduction: [0, 691.60, 270.40] - merton_council_tax_reduction_non_dep_deductions: [962, 0] - council_tax_reduction: 838 + # pays £13.30 on the couple's joint £576.92. Para 30(8)(c) exempts one + # entitled to UC "calculated on the basis that the person does not have + # any earned income", read on each person's own earnings as in the + # national schemes: the partner has none, so pays nothing; the worker is + # not exempt. + merton_council_tax_reduction_individual_non_dep_deduction: [0, 691.60, 0] + merton_council_tax_reduction_non_dep_deductions: [691.60, 0] + council_tax_reduction: 1_108.40 - name: Merton makes one deduction for a couple not on Universal Credit, on joint income period: 2026 @@ -496,9 +500,10 @@ council_tax: 1_800 savings: 0 output: - # Para 30B(3) has no Universal Credit exception: the higher of £22.61 - # and £7.54. - newham_council_tax_reduction_individual_non_dep_deduction: [0, 1_175.72, 392.08] + # Para 30B(3) has no Universal Credit exception: the higher of the two, + # the worker's £22.61. The partner, with no earned income of her own, is + # exempt under para 30B's Universal Credit exemption. + newham_council_tax_reduction_individual_non_dep_deduction: [0, 1_175.72, 0] newham_council_tax_reduction_non_dep_deductions: [1_175.72, 0] council_tax_reduction: 84.28 From f4589b9e94b7bdc26af8ced2fd30e267b85bc676 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:04:40 -0400 Subject: [PATCH 05/10] Exclude a non-dependant personally only as a joint tenant or a lodger outside the head's family Reg 9(2)(d)-(e) exclude the person, but reg 9(3)(a) keeps as a non-dependant anyone who pays a close relative of the applicant or partner, or pays on a non-commercial basis. The model has no relationship input, so a member of the household head's own family who pays board is taken to be a close relative and stays a non-dependant; anyone else who pays the householder is excluded (Sol review r1 finding 2, maintainer point 6). The gap for a close relative in another family is documented in the variable. The household head and anyone liable for the rent are no longer excluded wholesale: #2017's in-unit set is built on the CTR applicant, so an applicant is never in it, and is_liable_for_household_rent marks the young couple of a grandmother who applies alone as liable, which wrongly excluded them (maintainer point 3). A joint tenant who is not their unit's claimant or partner (an input) is still excluded. A student head whose family cannot claim stays the co-tenant's non-dependant, as main's case has it. No raw is_household_head read remains. Co-Authored-By: Claude Opus 5.5 --- ...n_individual_non_dep_deduction_eligible.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 148510528b..6a8608c938 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -18,9 +18,9 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): "liable for the council tax with the applicant (a joint tenant or " "other sharer of the rent) and a boarder or lodger who pays the " "applicant are not non-dependants. Those exclusions belong to the " - "person, whatever family they are in: a person the model treats as " - "liable for the council tax (council_tax_reduction_liable_person), " - "and a person outside the household head's family who pays the " + "person, whatever family they are in: a joint tenant liable for the " + "rent who is not their benefit unit's claimant or partner, and a " + "person outside the household head's family who pays the " "householder for their own board or lodging, is nobody's " "non-dependant. A member of the household head's own family who pays " "board is taken to be a close relative of the head, so remains a " @@ -47,9 +47,11 @@ def formula(person, period, parameters): in_unit = person( "is_benefit_unit_non_dependant_for_council_tax_reduction", period ) - # Reg 9(2)(d)-(e) exclude the person, not only their family. Anyone - # treated as liable for the council tax (LGFA 1992 s.6) is jointly - # and severally liable with the applicant. Anyone who pays the + # Reg 9(2)(d)-(e) exclude the person, not only their family. A joint + # tenant (the same interest in the dwelling, LGFA 1992 s.6) is + # jointly and severally liable with the applicant, even one who is + # not their benefit unit's claimant or partner; the applicants and + # partners of liable families are never in_unit. Anyone who pays the # householder for their own board or lodging is liable to make # payments on a commercial basis, even where the rest of their family # pays nothing, unless they are a close relative of the applicant or @@ -61,9 +63,10 @@ def formula(person, period, parameters): add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) > 0 ) in_head_family = person.benunit("benunit_contains_household_head", period) - personally_excluded = person("council_tax_reduction_liable_person", period) | ( - pays_householder & ~in_head_family + joint_tenant = person("is_liable_for_household_rent", period) & ~person( + "is_claimant_or_partner", period ) + personally_excluded = joint_tenant | (pays_householder & ~in_head_family) return ( (person("age", period) >= 18) & (other_family | in_unit) From 187a0596b8545ef0dff81ab47a7d1e7fa4f9391d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:04:40 -0400 Subject: [PATCH 06/10] Deduct for each member of a Universal Credit couple in Westminster, as the Default Scheme does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Westminster publishes only a summary of its working-age scheme, which states that the Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 form its basis, and the Appendix A scale. The Default Scheme's para 30(3) makes one deduction for a couple "(other than where there is an award of universal credit)", as Merton's and Kingston upon Thames's full texts do. With no Westminster provision departing from it, the model follows the Default Scheme (Sol review r1 finding 4, d1398 part 2, logged in RULINGS.md). A YAML case pins it: a UC couple, one earning £30,000 at 37.5 hours and one £2,600 at 10 hours, pays £15.95 + £5.20 a week (1,099.80 a year). The property oracle's Westminster entry follows. Co-Authored-By: Claude Opus 5.5 --- .../local_scheme_non_dependants.yaml | 51 +++++++++++++++++-- ...t_benefit_unit_non_dependant_properties.py | 17 ++++--- ...ouncil_tax_reduction_non_dep_deductions.py | 14 +++++ 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml index 64686f3ab5..704a355129 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -10,9 +10,10 @@ # Each non-dependant aged 18 or over is banded on their own normal gross weekly # income and is in remunerative work on their own hours (para 30(1)-(2) and # para 10); a couple's income is joint and the couple pays one deduction, the -# higher (para 30(3)-(4); Oxford para 44), except that Merton and Kingston -# upon Thames deduct for each member of a couple with a Universal Credit -# award. The benefit-receipt exemptions (para 30(8)) apply to the person on +# higher (para 30(3)-(4); Oxford para 44), except that Merton, Kingston upon +# Thames and Westminster deduct for each member of a couple with a Universal +# Credit award (the Default Scheme's para 30(3), on which Westminster's +# working-age scheme is based). The benefit-receipt exemptions (para 30(8)) apply to the person on # the award: Income Support, income-based JSA, income-related ESA and State # Pension Credit to the member they are payable to, and Universal Credit to # each joint claimant who has no earned income of their own. @@ -507,6 +508,50 @@ newham_council_tax_reduction_non_dep_deductions: [1_175.72, 0] council_tax_reduction: 84.28 +- name: Westminster deducts for each member of a Universal Credit couple, as the Default Scheme does + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + is_household_head: true + is_claimant_or_partner: true + worker: + age: 35 + is_claimant_or_partner: true + employment_income: 30_000 + weekly_hours: 37.5 + partner: + age: 35 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 10 + benunits: + claimant_benunit: + members: [claimant] + would_claim_uc: false + claims_all_entitled_benefits: true + couple: + members: [worker, partner] + universal_credit: 1_000 + households: + household: + members: [claimant, worker, partner] + country: ENGLAND + local_authority: WESTMINSTER + council_tax: 1_800 + savings: 0 + output: + # Westminster's working-age scheme is based on the Default Scheme, whose + # para 30(3) makes no single deduction where there is an award of + # Universal Credit. The worker pays £15.95 on the couple's joint £626.92 + # a week (Appendix A); the partner, earning but not in remunerative work, + # £5.20, and is not exempt, having earned income of her own. + westminster_council_tax_reduction_individual_non_dep_deduction: [0, 829.40, 270.40] + westminster_council_tax_reduction_non_dep_deductions: [1_099.80, 0] + council_tax_reduction: 700.20 + - name: Westminster charges the applicant's own non-dependant son period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index 88a92228dd..53dc8714aa 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -32,8 +32,8 @@ Oxford, each non-dependant's deduction and each claiming family's total equal an independent calculation from the council's scale: a claimant or partner banded on the couple's joint earnings and the couple deducted once - at the higher amount (each member in Merton and Kingston upon Thames - when the couple has Universal Credit), any other adult banded + at the higher amount (each member in Merton, Kingston upon Thames and + Westminster when the couple has Universal Credit), any other adult banded on their own earnings, each on their own hours; the benefit-receipt exemptions for the person on the award, Universal Credit's tested on their own earnings (none in Oxford); and the household's pool charged to each @@ -598,7 +598,10 @@ def other_families(amounts): # 5-6. The English working-age local schemes. Each council's weekly scale for # 2026-27 (the parameters' first values, which also apply in 2025): Merton # para 30, Kingston upon Thames para 30A, Newham para 30B, Westminster -# Appendix A and Oxford para 43. +# Appendix A and Oxford para 43. Westminster's working-age scheme is based on +# the Default Scheme (SI 2012/2886), whose para 30(3) deducts for each member +# of a couple with an award of Universal Credit, as Merton's and Kingston upon +# Thames's texts do. LOCAL_SCHEMES = { "MERTON": dict( name="merton", @@ -626,7 +629,7 @@ def other_families(amounts): thresholds=[0, 279, 485, 605], amounts=[5.20, 10.60, 13.30, 15.95], benefit_exemptions=True, - each_uc_couple_member=False, + each_uc_couple_member=True, ), "OXFORD": dict( name="oxford", @@ -790,9 +793,9 @@ def local_oracle(person_rows, family_rows, benefits, on_award, claims, ctr_share on_award["universal_credit"][p] and row["earnings"] <= 0 ) individual[p] = 0 if exempt else weekly * 52 - # One deduction for a couple, the higher, or each member's in Merton and - # Kingston upon Thames where the couple has Universal Credit; any other - # member separately. + # One deduction for a couple, the higher, or each member's in Merton, + # Kingston upon Thames and Westminster where the couple has Universal + # Credit; any other member separately. for f, family in enumerate(family_rows): members = [p for p, row in enumerate(person_rows) if row["family"] == f] couple = [p for p in members if person_rows[p]["claimant_or_partner"]] diff --git a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction_non_dep_deductions.py index feefaa2813..7efecd22ff 100644 --- a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction_non_dep_deductions.py @@ -8,12 +8,26 @@ class westminster_council_tax_reduction_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "Westminster CTR non-dependent deductions" + documentation = ( + "Westminster's working-age scheme is based on the Council Tax " + "Reduction Schemes (Default Scheme) (England) Regulations 2012, and " + "its published summary sets out only the scale (Appendix A). The " + "Default Scheme's para 30(3) makes one deduction for a couple, the " + "higher, other than where there is an award of Universal Credit, so " + "each member of a non-dependant couple with Universal Credit is " + "deducted for, as in Merton's and Kingston upon Thames's texts." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.westminster.gov.uk/sites/default/files/media/documents/council-tax-support-scheme-for-2026-27.pdf", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/30", + ) def formula(benunit, period, parameters): return local_non_dep_deductions( benunit, period, "westminster_council_tax_reduction_individual_non_dep_deduction", + one_deduction_for_uc_couples=False, ) From aaf01f9685f98203791fdaff275e0774c5aa9f06 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:13:02 -0400 Subject: [PATCH 07/10] Test the close-relative presumption across the schemes and restate the changelog Sol review r1 finding 2 asked for regression coverage across the national and devolved schemes, since the eligibility variable is shared. A son in his mother's benefit unit who pays her board stays her non-dependant in England's pensioner scheme (270.40), Scotland's (288.60), Wales's (348.40) and Merton's (691.60 on his own earnings); a lodger's son who pays the householder himself is excluded in Scotland. The relationship gap is #2559. The changelog now says what changes in the national schemes too (maintainer point 5): who is excluded for paying board or as a joint tenant. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2014-local.fixed.md | 2 +- .../family/benefit_unit_non_dependants.yaml | 121 ++++++++++++++++++ .../local_scheme_non_dependants.yaml | 43 ++++++- 3 files changed, 161 insertions(+), 5 deletions(-) diff --git a/changelog.d/2014-local.fixed.md b/changelog.d/2014-local.fixed.md index 129c493722..29cf62eda1 100644 --- a/changelog.d/2014-local.fixed.md +++ b/changelog.d/2014-local.fixed.md @@ -1 +1 @@ -Charge non-dependant deductions in the English working-age local Council Tax Reduction schemes (Merton, Kingston upon Thames, Newham, Westminster, Oxford) for adults in the applicant's own benefit unit, deduct separately for each adult in another family who is not its claimant or partner, band each on their own income and hours, and apply benefit-receipt exemptions only to the award's claimant and partner. +Charge non-dependant deductions in the English working-age local Council Tax Reduction schemes (Merton, Kingston upon Thames, Newham, Westminster, Oxford) as the national schemes do: for adults in the applicant's own benefit unit (the applicant identified as for the means test, so a grandmother who applies alone is charged for the couple in her benefit unit), separately for each adult in another family who is not its claimant or partner, and apportioned between jointly liable applicants. Band a claimant or partner on the couple's joint income and anyone else on their own, each on their own hours; exempt a non-dependant on Income Support, income-based JSA, income-related ESA or State Pension Credit only if the award is payable to them, and one on Universal Credit only if they have no earned income of their own; and deduct for each member of a Universal Credit couple in Westminster, as the Default Scheme does. In every scheme, national and local, someone who pays the householder for their own board or lodging, or is a joint tenant, is not a non-dependant even where the rest of their family is, except a member of the household head's own family who pays board, who is taken to be a close relative. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index 61a9153226..55ffc05f16 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -1362,3 +1362,124 @@ output: uc_non_dependants_counted: [false, true] uc_non_dep_deductions: [0, 1_158.60] + +# SI 2012/2885 reg 9(2)(e) excludes someone liable to pay the applicant on a +# commercial basis, but reg 9(3)(a)(i) keeps as a non-dependant one who is a +# close relative of the applicant or partner (#2559 checks the Welsh and +# Scottish equivalents' wording). The model has no relationship +# input: a member of the household head's own family who pays board is taken +# to be a close relative, so stays a non-dependant. The son is not in +# remunerative work, so pays each scheme's lowest amount for 2026-27. +# England's pensioner scheme: 5.20 a week (SI 2012/2885 Sch 1 para 8(1)(b)). +- name: CTR (England pensioners), a son who pays his mother board stays her non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 70 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + rent_paid_as_boarder: 2_600 + benunits: + benunit: + members: [mother, son] + households: + household: + members: [mother, son] + country: ENGLAND + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deductions: 270.40 + +# Scotland's working-age scheme: 5.55 a week (SSI 2021/249 reg 90). +- name: CTR (Scotland), a son who pays his mother board stays her non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + rent_paid_as_boarder: 2_600 + benunits: + benunit: + members: [mother, son] + households: + household: + members: [mother, son] + country: SCOTLAND + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deductions: 288.60 + +# Wales's working-age scheme: 6.70 a week (WSI 2013/3029 Sch 6 para 5). +- name: CTR (Wales), a son who pays his mother board stays her non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + rent_paid_as_boarder: 2_600 + benunits: + benunit: + members: [mother, son] + households: + household: + members: [mother, son] + country: WALES + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deductions: 348.40 + +# Someone outside the household head's family who pays the householder is +# excluded personally (reg 9(2)(e)), even where the rest of their family pays +# nothing; the lodger's son who does not pay is still a non-dependant (see +# "CTR, a lodger's adult son is the applicant's non-dependant"). +- name: CTR (Scotland), a lodger's adult son who pays the householder himself is not a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 45 + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + lodger_son: + age: 21 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + rent_paid_as_lodger: 2_600 + benunits: + benunit_1: + members: [head] + benunit_2: + members: [lodger, lodger_son] + households: + household: + members: [head, lodger, lodger_son] + rent: 9_000 + country: SCOTLAND + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false] + council_tax_reduction_non_dep_deductions: [0, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml index 704a355129..4b6b546772 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/local_scheme_non_dependants.yaml @@ -759,10 +759,11 @@ oxford_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 691.60] council_tax_reduction: 838 -# Para 9(2)(d)-(e) exclude the person, whatever family they are in: someone who -# pays the householder as a boarder or lodger, someone jointly liable for the -# council tax, and the liable person (the household head) are nobody's -# non-dependants. +# Para 9(2)(d)-(e) exclude the person, whatever family they are in: someone +# outside the household head's family who pays the householder as a boarder +# or lodger, and a joint tenant, are nobody's non-dependants. Para 9(3)(a)(i) +# keeps as a non-dependant a close relative who pays; with no relationship +# input, a member of the head's own family who pays board is taken to be one. - name: Merton makes no deduction for an adult son who himself pays as a lodger period: 2026 @@ -842,6 +843,40 @@ merton_council_tax_reduction_non_dep_deductions: [691.60, 0] council_tax_reduction: 1_108.40 +- name: Merton charges the applicant's own son who pays her board, as a close relative + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + rent_paid_as_boarder: 2_600 + employment_income: 30_000 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, son] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [parent, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Para 30: £13.30 a week on £576.92 (para 9(3)(a)(i) keeps him a + # non-dependant although he pays). + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + merton_council_tax_reduction_non_dep_deductions: 691.60 + council_tax_reduction: 1_108.40 + - name: Merton makes no deduction for an adult son who is himself a joint tenant period: 2026 absolute_error_margin: 0.01 From 1a71c4a699fc3cd0171b8e4d1358f3a61046fbb4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:26:30 -0400 Subject: [PATCH 08/10] Settle #2078's provisional grandmother cases now that in-unit non-dependants are charged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each case said it was provisional until #2017: the 19-year-old parents are the grandmother's non-dependants, but the model charged none in her benefit unit. They passed only because the couple defaulted to tertiary education, so the full-time student exemption applied (maintainer review point 3). The couple are now not in education, and the Newham father works 37.5 hours: - England pensioner (JSA): the payee is exempt, the other pays the lowest amount, 270.40, so the reduction is 489.20, not 759.60. - Newham (UC): one deduction, the father's 22.61 a week on £576.92 (1,175.72); the mother has no earned income of her own, so is exempt. The reduction is 84.28, as the maintainer's probe gave. - Kingston upon Thames (UC): both on UC with no earned income, so both are exempt; the reduction is unchanged at 1,627.32. Co-Authored-By: Claude Opus 5.5 --- .../applicant_or_partner.yaml | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml index 405fc1ffc3..d4f30bd840 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml @@ -140,10 +140,8 @@ simulated_council_tax_reduction_benunit: [759.60] - name: The young couple's income-based Jobseeker's Allowance does not passport the grandmother head - # Provisional until in-unit non-dependants land (#2017): the 19-year-old - # parents are her non-dependants (SI 2012/2885 reg 9(1); they are not in - # her family), but the model charges a deduction only for non-dependants - # in another benefit unit, so this case has none. + # The 19-year-old parents are her non-dependants (SI 2012/2885 reg 9(1); + # they are not in her family), and neither is in education. period: 2026 absolute_error_margin: 0.01 input: @@ -156,10 +154,12 @@ age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION young_father: age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION baby: age: 0 is_household_head: false @@ -183,7 +183,12 @@ council_tax_reduction_relevant_income_based_benefit: [false] # Neither the couple's JSA nor their Child Benefit is her income. council_tax_reduction_applicable_income: [18_514] - simulated_council_tax_reduction_benunit: [759.60] + # 759.60 before non-dependant deductions. The JSA is payable to one of + # the couple, who is exempt (Sch 1 para 8(8)); the other, not in + # remunerative work, pays the lowest amount, 5.20 a week, the couple's + # one deduction (para 8(3)): 270.40. + council_tax_reduction_non_dep_deductions: [270.40] + simulated_council_tax_reduction_benunit: [489.20] - name: A head who applies alone is a single person, with premiums on her own disability only period: 2026 @@ -323,10 +328,8 @@ merton_council_tax_reduction: [1_800, 0] - name: The young couple's Universal Credit does not put a grandmother head on Newham's Universal Credit route - # Provisional until in-unit non-dependants land (#2017): the 19-year-old - # parents are her non-dependants (SI 2012/2885 reg 9(1); they are not in - # her family), but the model charges a deduction only for non-dependants - # in another benefit unit, so this case has none. + # The 19-year-old parents are her non-dependants (para 9(1); they are not + # in her family), and neither is in education. period: 2026 absolute_error_margin: 0.01 input: @@ -338,10 +341,13 @@ age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION young_father: age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION + weekly_hours: 37.5 employment_income: 30_000 baby: age: 0 @@ -365,9 +371,13 @@ council_tax_reduction_applicable_income: [0] # Single aged 25 or over: 95.55 * 52. council_tax_reduction_applicable_amount: [4_968.60] - # No excess income: 1,800 * 70%. On the Universal Credit route the - # couple's earnings and award would leave nothing. - newham_council_tax_reduction: [1_260] + # No excess income: 1,800 * 70% = 1,260. On the Universal Credit route + # the couple's earnings and award would leave nothing. The couple pay one + # deduction (para 30B(3)): the father's £22.61 a week on their joint + # £576.92, 1,175.72 a year; the mother, on Universal Credit with no + # earned income of her own, is exempt. + newham_council_tax_reduction_non_dep_deductions: [1_175.72] + newham_council_tax_reduction: [84.28] - name: A partner under 18 is not liable, but their income counts in the couple's means test period: 2026 @@ -495,10 +505,10 @@ council_tax_reduction_applicable_amount: [7_807.80] - name: The young couple's Universal Credit does not remove a grandmother head's tariff income from capital in Kingston upon Thames - # Provisional until in-unit non-dependants land (#2017): the 19-year-old - # parents are her non-dependants (SI 2012/2885 reg 9(1); they are not in - # her family), but the model charges a deduction only for non-dependants - # in another benefit unit, so this case has none. + # The 19-year-old parents are her non-dependants (para 9(1); they are not + # in her family), and neither is in education. Both are on the couple's + # Universal Credit with no earned income, so both are exempt under para + # 30A's Universal Credit exemption, and there is no deduction. period: 2026 absolute_error_margin: 0.01 input: @@ -511,10 +521,12 @@ age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION young_father: age: 19 is_parent: true is_household_head: false + current_education: NOT_IN_EDUCATION baby: age: 0 is_household_head: false @@ -537,6 +549,7 @@ council_tax_reduction_applicable_amount: [4_968.60] # Tariff income ceil((10,000 - 6,000) / 250) = £16 a week, 832 a year, # since she has no award. 1,800 - (5,000 + 832 - 4,968.60) * 20%. + kingston_upon_thames_council_tax_reduction_non_dep_deductions: [0] kingston_upon_thames_council_tax_reduction: [1_627.32] - name: A blind grandmother head's exemption does not exempt a sharer's claim from a non-dependant deduction in Merton From cf0475ddb1498a2f6fd969e5e63fc80943134105 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:28:59 -0400 Subject: [PATCH 09/10] Cite what the Default Scheme defines for being on each award Para 2(3)-(4) define being on income-based JSA and income-related ESA as the award being payable to the person, and para 2(1) a person on Income Support as one in receipt of it; State Pension Credit has no definition, so the model reads it the same way. Comment only. Co-Authored-By: Claude Opus 5.5 --- .../local_authorities/council_tax_reduction/_legacy.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index f96f70755a..87eaba23a1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -191,10 +191,12 @@ def normal_gross_income_non_dep_deduction( ) full_time_student = is_full_time_student_non_dep(person, period) # No deduction for a non-dependant "who is on" Income Support, State - # Pension Credit, income-based JSA or income-related ESA (para 30(8)(a)): - # an award payable to them (Default Scheme Sch para 2(3)-(4)). Of a couple, - # only the payee is on the couple's award, and another member of their - # benefit unit is on it only through an award of their own. + # Pension Credit, income-based JSA or income-related ESA (para 30(8)(a)). + # A person is on income-based JSA or income-related ESA when it is payable + # to them (Default Scheme Sch para 2(3)-(4)) and on Income Support when in + # receipt of it (para 2(1)); the model reads State Pension Credit the same + # way. Of a couple, only the payee is on the couple's award, and another + # member of their benefit unit is on it only through an award of their own. income_based_benefit = ( person("is_on_income_support", period) | person("is_on_income_based_jsa", period) From a5231bac05d1cee7754342434034ca5cd27df489 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 9 Nov 2026 10:28:07 -0500 Subject: [PATCH 10/10] Count a lodger's young person in the local schemes' oracle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local schemes' oracle left out a lodger's qualifying young person aged 18 or over, whom the model and the national oracle count as a non-dependant: reg 9(2)(e) of SI 2012/2885 excludes only the person liable to pay the householder. CI found a Westminster household where the model charged such a young person £270.40 a year and the oracle charged nothing. Co-Authored-By: Claude Opus 5.5 --- .../test_benefit_unit_non_dependant_properties.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index 581ae5aba3..64e58ed21e 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -766,9 +766,16 @@ def local_oracle(person_rows, family_rows, benefits, on_award, claims, ctr_share scheme = LOCAL_SCHEMES[row["local_authority"]] # Reg 9: a member of a family not liable for the rent, or an adult in # any family's benefit unit who is not its applicant, partner, child - # or young person; aged 18 or over. + # or young person; aged 18 or over. Only a lodger and partner are + # liable to pay the householder (reg 9(2)(e)), so the lodger's + # qualifying young person is a non-dependant too. eligible = row["age"] >= 18 and ( - family_rows[f]["role"] == "non_dependant" or row["kind"] == "non_dependant" + family_rows[f]["role"] == "non_dependant" + or row["kind"] == "non_dependant" + or ( + family_rows[f]["role"] == "lodger" + and row["kind"] in ("dependant", "looked_after") + ) ) if not eligible or row["student"]: continue