From 69b2703d7b5dc1cb185f18770d050c4f51a3565b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 16:02:07 -0400 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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 + )