diff --git a/changelog.d/ctr-claimant-household-head.fixed.md b/changelog.d/ctr-claimant-household-head.fixed.md new file mode 100644 index 0000000000..9ec474fa33 --- /dev/null +++ b/changelog.d/ctr-claimant-household-head.fixed.md @@ -0,0 +1 @@ +Give Council Tax Reduction to the household head's family, the liable resident, instead of the oldest adult's, so families tied for oldest no longer both get a full award. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml new file mode 100644 index 0000000000..089272e357 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml @@ -0,0 +1,721 @@ +# Who claims Council Tax Reduction in a household of several families. +# Only a person liable to pay the council tax is in a class entitled to a +# reduction: England SI 2012/2885 Sch 1 paras 2(a) and 3(a); Wales SI +# 2013/3029 regs 22(a) to 25(a); Scotland SSI 2021/249 reg 13(1) and (3)(a) +# and SSI 2012/319 reg 14(1) and (3)(a). The liable resident is the one with +# the greatest interest in the dwelling (Local Government Finance Act 1992 +# s.6 in England and Wales, s.75 in Scotland), not the eldest. The model +# takes the household head (the household reference person, a householder) +# as that person, so the head's family claims and every other adult is a +# non-dependant (SI 2012/2885 reg 9; SI 2013/3029 reg 9; SSI 2021/249 reg 8). +# Joint liability where the rent is shared: joint_liability.yaml. + +- name: Two single pensioners of the same age in England - only the head's family claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + friend: + age: 85 + is_household_head: false + council_tax_reduction_individual_non_dep_deduction: 0 + householder: + age: 85 + is_household_head: true + benunits: + friend_family: + members: [friend] + council_tax_reduction_applicable_income: 0 + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [friend, householder] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + benunit_contains_household_head: [false, true] + council_tax_reduction_claimant_benunit: [false, true] + council_tax_reduction_individual_non_dep_deduction_eligible: [true, false] + simulated_council_tax_reduction_benunit: [0, 1_800] + # One award of the council tax, not two. + council_tax_reduction: 1_800 + +- name: Two single pensioners of the same age in Scotland with no head flagged - one family claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + first: + age: 85 + council_tax_reduction_individual_non_dep_deduction: 0 + second: + age: 85 + council_tax_reduction_individual_non_dep_deduction: 0 + benunits: + first_family: + members: [first] + council_tax_reduction_applicable_income: 0 + second_family: + members: [second] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [first, second] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # With no input, the eldest member is the head, ties by order. + is_household_head: [true, false] + council_tax_reduction_claimant_benunit: [true, false] + simulated_council_tax_reduction_benunit: [1_800, 0] + council_tax_reduction: 1_800 + +- name: Two single pensioners of the same age in Wales - only the head's family claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 85 + is_household_head: true + friend: + age: 85 + is_household_head: false + council_tax_reduction_individual_non_dep_deduction: 0 + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + friend_family: + members: [friend] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [householder, friend] + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_claimant_benunit: [true, false] + simulated_council_tax_reduction_benunit: [1_800, 0] + council_tax_reduction: 1_800 + +- name: Two working-age adults of the same age in Newham - the other adult is a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + friend: + age: 40 + is_household_head: false + householder: + age: 40 + is_household_head: true + benunits: + friend_family: + members: [friend] + would_claim_uc: false + claims_all_entitled_benefits: true + householder_family: + members: [householder] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + home: + members: [friend, householder] + country: ENGLAND + local_authority: NEWHAM + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_claimant_benunit: [false, true] + # The friend, not in remunerative work: 7.54 a week x 52 = 392.08. + newham_council_tax_reduction_non_dep_deductions: [0, 392.08] + # 1,800 x 70% - 392.08 = 867.92, once. + newham_council_tax_reduction: [0, 867.92] + council_tax_reduction: 867.92 + +- name: Two working-age adults of the same age in Merton - the other adult is a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + friend: + age: 40 + is_household_head: false + benunits: + householder_family: + members: [householder] + would_claim_uc: false + claims_all_entitled_benefits: true + friend_family: + members: [friend] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + home: + members: [householder, friend] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # The friend, not in remunerative work: 5.20 a week x 52 = 270.40. + merton_council_tax_reduction_non_dep_deductions: [270.4, 0] + merton_council_tax_reduction: [1_529.6, 0] + council_tax_reduction: 1_529.6 + +- name: Two working-age adults of the same age in Kingston upon Thames - the other adult is a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + friend: + age: 40 + is_household_head: false + benunits: + householder_family: + members: [householder] + would_claim_uc: false + claims_all_entitled_benefits: true + friend_family: + members: [friend] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + home: + members: [householder, friend] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + council_tax: 1_800 + savings: 0 + output: + # The friend, not in remunerative work: 5.64 a week x 52 = 293.28. + kingston_upon_thames_council_tax_reduction_non_dep_deductions: [293.28, 0] + kingston_upon_thames_council_tax_reduction: [1_506.72, 0] + council_tax_reduction: 1_506.72 + +- name: Two working-age adults of the same age in Westminster - the other adult is a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + friend: + age: 40 + is_household_head: false + benunits: + householder_family: + members: [householder] + would_claim_uc: false + claims_all_entitled_benefits: true + friend_family: + members: [friend] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + home: + members: [householder, friend] + country: ENGLAND + local_authority: WESTMINSTER + council_tax: 1_800 + savings: 0 + output: + # The friend, not in remunerative work: 5.20 a week x 52 = 270.40. + westminster_council_tax_reduction_non_dep_deductions: [270.4, 0] + westminster_council_tax_reduction: [1_529.6, 0] + council_tax_reduction: 1_529.6 + +- name: Two working-age adults of the same age in Oxford - the other adult is a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + friend: + age: 40 + is_household_head: false + benunits: + householder_family: + members: [householder] + would_claim_uc: false + claims_all_entitled_benefits: true + friend_family: + members: [friend] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + home: + members: [householder, friend] + country: ENGLAND + local_authority: OXFORD + council_tax: 1_800 + savings: 0 + output: + # The friend, not in remunerative work: 5.20 a week x 52 = 270.40. + oxford_council_tax_reduction_non_dep_deductions: [270.4, 0] + oxford_council_tax_reduction: [1_529.6, 0] + council_tax_reduction: 1_529.6 + +- name: A pensioner living in their adult child's home in Scotland is a non-dependant, not the claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + parent: + age: 80 + is_household_head: false + council_tax_reduction_individual_non_dep_deduction: 200 + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + parent_family: + members: [parent] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [householder, parent] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # The eldest adult is not the liable resident; the head is. + benunit_contains_household_head: [true, false] + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_household_has_pensioner: false + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deductions: [200, 0] + simulated_council_tax_reduction_benunit: [1_600, 0] + council_tax_reduction: 1_600 + +- name: A pensioner in their working-age child's home in England does not put the household on the pensioner scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + parent: + age: 80 + is_household_head: false + attendance_allowance: 5_000 + benunits: + householder_family: + members: [householder] + would_claim_uc: false + parent_family: + members: [parent] + households: + home: + members: [householder, parent] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + # The claimant's family is working-age; Maidstone's working-age scheme is + # not modelled, so no simulated reduction. + council_tax_reduction_household_has_pensioner: false + council_tax_reduction_scheme_supported: false + simulated_council_tax_reduction_benunit: [0, 0] + # A non-dependant's Attendance Allowance does not exempt the claimant + # from non-dependant deductions (SI 2012/2885 Sch 1 para 8(6)). + council_tax_reduction_household_has_non_dep_exemption: false + +- name: With two members flagged as head, the eldest flagged member's family claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + younger_flagged: + age: 60 + is_household_head: true + eldest_unflagged: + age: 90 + is_household_head: false + older_flagged: + age: 70 + is_household_head: true + benunits: + younger_family: + members: [younger_flagged] + eldest_family: + members: [eldest_unflagged] + older_family: + members: [older_flagged] + households: + home: + members: [younger_flagged, eldest_unflagged, older_flagged] + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + output: + benunit_contains_household_head: [false, false, true] + council_tax_reduction_claimant_benunit: [false, false, true] + +- name: With no member flagged as head and the eldest tied, the first eldest member's family claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + first_eldest: + age: 50 + is_household_head: false + younger: + age: 30 + is_household_head: false + second_eldest: + age: 50 + is_household_head: false + benunits: + first_family: + members: [first_eldest] + younger_family: + members: [younger] + second_family: + members: [second_eldest] + households: + home: + members: [first_eldest, younger, second_eldest] + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + output: + benunit_contains_household_head: [true, false, false] + council_tax_reduction_claimant_benunit: [true, false, false] + +- name: A sharer with no rent to pay is a claimant, not a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + sharer: + age: 35 + is_household_head: false + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [householder, sharer] + rent: 0 + tenure_type: RENT_PRIVATELY + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_claimant_benunit: [true, true] + council_tax_reduction_joint_liability_share: [0.5, 0.5] + # Jointly and severally liable people are not non-dependants (SI 2013/3029 + # reg 9(2)(d)), even when no rent is payable. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] + simulated_council_tax_reduction_benunit: [900, 900] + council_tax_reduction: 1_800 + +- name: With no member flagged as head and the rent shared, the eldest member's family and the sharer split the council tax + period: 2026 + absolute_error_margin: 0.01 + input: + people: + eldest: + age: 60 + is_household_head: false + sharer: + age: 40 + is_household_head: false + other_adult: + age: 30 + is_household_head: false + council_tax_reduction_individual_non_dep_deduction: 0 + benunits: + eldest_family: + members: [eldest] + council_tax_reduction_applicable_income: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + other_family: + members: [other_adult] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [eldest, sharer, other_adult] + rent: 9_000 + tenure_type: RENT_PRIVATELY + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_claimant_benunit: [true, true, false] + # Two people jointly liable: the head (by the eldest-member fallback) and + # the sharer. The shares sum to one. + council_tax_reduction_joint_liability_share: [0.5, 0.5, 1] + simulated_council_tax_reduction_benunit: [900, 900, 0] + council_tax_reduction: 1_800 + +- name: With two members flagged as head and the rent shared, the council tax is still split only among claimants + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older_flagged: + age: 60 + is_household_head: true + younger_flagged: + age: 50 + is_household_head: true + sharer: + age: 40 + is_household_head: false + benunits: + older_family: + members: [older_flagged] + council_tax_reduction_applicable_income: 0 + younger_family: + members: [younger_flagged] + council_tax_reduction_applicable_income: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [older_flagged, younger_flagged, sharer] + rent: 9_000 + tenure_type: RENT_PRIVATELY + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + # Malformed input. The eldest flagged member is the head; the other + # flagged family neither claims nor counts as jointly liable. (Rent + # variables read the raw flag, so that family still has a share of the + # rent.) + council_tax_reduction_claimant_benunit: [true, false, true] + council_tax_reduction_joint_liability_share: [0.5, 1, 0.5] + simulated_council_tax_reduction_benunit: [900, 0, 900] + council_tax_reduction: 1_800 + +- name: A sharer family with no adult does not claim, so the head's family claims the whole council tax + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + child_sharer: + age: 15 + is_household_head: false + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + child_family: + members: [child_sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [householder, child_sharer] + rent: 9_000 + tenure_type: RENT_PRIVATELY + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + # Only a resident aged 18 or over can be liable (LGFA 1992 s.6(5)). + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_joint_liability_share: [1, 1] + simulated_council_tax_reduction_benunit: [1_800, 0] + council_tax_reduction: 1_800 + +- name: A sharer family whose only member is 17 does not claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + young_sharer: + age: 17 + is_household_head: false + benunits: + householder_family: + members: [householder] + council_tax_reduction_applicable_income: 0 + young_family: + members: [young_sharer] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [householder, young_sharer] + rent: 9_000 + tenure_type: RENT_PRIVATELY + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_joint_liability_share: [1, 1] + simulated_council_tax_reduction_benunit: [1_800, 0] + +- name: A grandparent household head whose family includes a young couple of 17-year-old parents claims + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + young_mother: + age: 17 + is_parent: true + is_household_head: false + young_father: + age: 17 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # The young parents are the family's claimant and partner, but the + # grandmother is the household head and the only resident aged 18 or + # over, so she is the liable person (LGFA 1992 s.75, s.99(1)). + is_claimant_or_partner: [false, true, true, false] + council_tax_reduction_liable_person: [true, false, false, false] + council_tax_reduction_claimant_benunit: [true] + council_tax_reduction_joint_liability_share: [1] + simulated_council_tax_reduction_benunit: [1_800] + council_tax_reduction: 1_800 + +- name: Adult parents living in a grandparent head's family are not jointly liable with her + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 75 + is_household_head: true + mother: + age: 30 + is_parent: true + is_household_head: false + father: + age: 32 + is_parent: true + is_household_head: false + child: + age: 4 + is_household_head: false + benunits: + family: + members: [grandmother, mother, father, child] + council_tax_reduction_applicable_income: 0 + households: + home: + members: [grandmother, mother, father, child] + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + # Only the head and the head's own partner are liable; the parents are + # not the grandmother's partners (LGFA 1992 s.6, s.9). + council_tax_reduction_liable_person: [true, false, false, false] + council_tax_reduction_claimant_benunit: [true] + council_tax_reduction_joint_liability_share: [1] + +- name: Where the rent is shared, a student head's family does not claim and the co-tenant claims the whole council tax + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_household_head: true + in_HE: true + cotenant: + age: 30 + is_household_head: false + benunits: + head_family: + members: [head] + cotenant_family: + members: [cotenant] + liable_for_share_of_household_rent: true + council_tax_reduction_applicable_income: 0 + households: + home: + members: [head, cotenant] + rent: 9_000 + tenure_type: RENT_PRIVATELY + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # The head is treated as liable, but where the rent is shared the model + # gives a student no claim (#2006; in Scotland SSI 2021/249 reg 20(2)) + # and leaves them out of those jointly liable (reg 79(4)). Where a + # student shares liability with a non-student, the non-student is solely + # liable (LGFA 1992 s.75(4)), so the co-tenant claims on the whole bill + # and the head is the co-tenant's non-dependant. No deduction is made for + # a full-time student non-dependant (reg 90(7)(c)); the model's national + # deduction is nil here only because the head has no income. + council_tax_reduction_liable_person: [true, true] + council_tax_reduction_claimant_benunit: [false, true] + council_tax_reduction_joint_liability_share: [0.5, 1] + council_tax_reduction_individual_non_dep_deduction_eligible: [true, false] + simulated_council_tax_reduction_benunit: [0, 1_800] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml index 0f46bd5af0..b8886db438 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/joint_liability.yaml @@ -155,7 +155,7 @@ council_tax_reduction_joint_liability_share: [1, 1] simulated_council_tax_reduction_benunit: [1_800, 0] -- name: An older parent who is not liable stays the claimant where no rent is shared +- name: An older parent who is not liable is the head's non-dependant where no rent is shared period: 2025 absolute_error_margin: 0.01 input: @@ -181,10 +181,14 @@ country: ENGLAND region: LONDON output: - # No sharer: the oldest adult's family claims, as before, and the - # rent-paying head is the father's non-dependant, as before. - council_tax_reduction_claimant_benunit: [false, true] - council_tax_reduction_individual_non_dep_deduction_eligible: [true, false] + # No sharer: the household head's family claims, as the resident liable + # for the council tax (LGFA 1992 s.6), however old the father is, and the + # father is the head's non-dependant (SI 2012/2885 reg 9). No local + # authority is given, so the head's working-age claim falls under the + # default council's (Maidstone's) scheme, which the model does not + # simulate. + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] simulated_council_tax_reduction_benunit: [0, 0] - name: A non-liable resident is a non-dependant of the sharers however old diff --git a/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py new file mode 100644 index 0000000000..09af6f3d4f --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py @@ -0,0 +1,384 @@ +"""Property-based tests for who claims Council Tax Reduction in a household +of several families. + +Only a person liable to pay the council tax is in a class entitled to a +reduction (SI 2012/2885 Sch 1 paras 2-3; SI 2013/3029 regs 22-25; SSI +2021/249 reg 13; SSI 2012/319 reg 14). The model takes the household head +(the household reference person, a householder) as the liable resident, and +families liable for a share of the rent as jointly liable with the head's. + +Where the rent is shared, a full-time student is excluded from entitlement +(Default Scheme Sch para 75(1)); the model takes a person in higher education +as one, so a family there claims only through a liable non-student. + +Invariants, for any generated population of households, including ties in +age, no head flagged, several heads flagged, sharer families with no member +aged 18 or over, and adults in higher education. Invariants marked "pin" +restate the formula, so they guard against regressions rather than check it +independently. + +1. One head: exactly one family in each household contains the household + head. With one or more members flagged, it holds a flagged member; with + none, it holds a member of the greatest age. +2. Claimants: a household whose rent is not shared has at most one claimant + family, the head's, and the head's family claims whenever the head is + 18 or over and, where the rent is shared, not a student (including a + grandparent head whose family's claimant and partner are 17-year-old + parents). A family with no member aged 18 or over never claims (no one + under 18 can be liable, LGFA 1992 s.6(5)), and where the rent is shared + no family claims through students alone. Every person treated as liable + is 18 or over and in the head's family or a sharer family. Pin: a family + claims if and only if it has a member treated as liable who, where the + rent is shared, is not a student. +3. Simulated reductions: no family outside the claimants gets one, and a + household's total never exceeds its council tax. (Reported reductions, + used where a scheme is not modelled, are outside this test.) +4. Non-dependants: no claimant or partner of a claimant family is a + non-dependant (the applicant's family, SI 2012/2885 reg 9(2)(a)). Pin: no + member of a sharer family is one, and every adult outside the claimant + families, the sharer families and boarders' and lodgers' families is one. +5. Differential: where the input flags at most one head, the family holding + the head is the one holding the person-level household head that Housing + Benefit and Universal Credit use. +6. Not age: with one head flagged, raising every other member's age above + the head's does not change who claims. +7. No-op: where the flagged head is strictly the eldest member and the rent + is not shared, the claimant family is the eldest adult's family, the + previous rule. +8. Shares: in every household, the claimant families' shares of the council + tax sum to at most one, so jointly liable claims never cover more than + the whole council tax. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=20, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +SCHEMES = [ + ("ENGLAND", "MAIDSTONE"), + ("ENGLAND", "MERTON"), + ("ENGLAND", "KINGSTON_UPON_THAMES"), + ("ENGLAND", "NEWHAM"), + ("ENGLAND", "WESTMINSTER"), + ("ENGLAND", "OXFORD"), + ("SCOTLAND", "CITY_OF_EDINBURGH"), + ("WALES", "CARDIFF"), +] +HEAD_FLAGS = ["one", "unset", "none", "several"] +# Ages around State Pension age and repeated values, so ties are common. +adult_age = st.one_of( + st.sampled_from([18, 25, 40, 66, 67, 80, 85]), st.integers(18, 100) +) +money = st.floats(0, 40_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def family(draw): + return dict( + ages=draw(st.lists(adult_age, min_size=1, max_size=2)), + child_age=draw(st.one_of(st.none(), st.integers(0, 15))), + earnings=draw(st.lists(money, min_size=2, max_size=2)), + # Each adult is sometimes in higher education. + students=draw( + st.lists(st.sampled_from([False, False, True]), min_size=2, max_size=2) + ), + sharer=draw(st.booleans()), + # Outside the first family: sometimes a single person aged 15-17. + minor_age=draw(st.one_of(st.none(), st.none(), st.integers(15, 17))), + # In the first family: sometimes a couple of parents aged 16-17 with + # a baby, living in the head's family (#1896's parent-couple rule). + minor_parents=draw(st.one_of(st.none(), st.none(), st.integers(16, 17))), + ) + + +def adult_ages(fam, f): + """The family's adults (none for a minor-only family after the first).""" + return [] if f > 0 and fam["minor_age"] is not None else fam["ages"] + + +@st.composite +def households(draw): + families = draw(st.lists(family(), min_size=1, max_size=4)) + adults = sum(len(adult_ages(fam, f)) for f, fam in enumerate(families)) + return dict( + families=families, + scheme=draw(st.sampled_from(SCHEMES)), + council_tax=draw(st.floats(0, 4_000, allow_nan=False)), + rent=draw(money), + savings=draw(st.floats(0, 20_000, allow_nan=False)), + head_flags=draw(st.sampled_from(HEAD_FLAGS)), + # Which adult is flagged ("one"), or which are ("several"). + head_choice=draw(st.lists(st.integers(0, adults - 1), min_size=1)), + ) + + +population = st.lists(households(), min_size=1, max_size=8) + + +def build(population, age_override=None): + """One situation for the whole population, plus per-person facts. + + age_override(house, adult_index, age, flagged) -> new age, if given. + """ + people, benunits, homes = {}, {}, {} + facts = dict(flagged=[], age=[], household=[], benunit=[], sharer=[]) + benunit_index = 0 + for h, house in enumerate(population): + country, local_authority = house["scheme"] + if house["head_flags"] == "one": + flagged_adults = {house["head_choice"][0]} + elif house["head_flags"] == "several": + flagged_adults = set(house["head_choice"]) + else: + flagged_adults = set() + members, adult_index = [], 0 + for f, fam in enumerate(house["families"]): + ids = [] + for i, age in enumerate(adult_ages(fam, f)): + pid = f"h{h}_f{f}_adult_{i}" + flagged = adult_index in flagged_adults + if age_override is not None: + age = age_override(house, adult_index, age, flagged) + person = { + "age": age, + "employment_income": fam["earnings"][i], + "in_HE": fam["students"][i], + } + if house["head_flags"] != "unset": + person["is_household_head"] = flagged + people[pid] = person + ids.append(pid) + facts["flagged"].append(flagged) + facts["age"].append(age) + adult_index += 1 + if f == 0 and fam["minor_parents"] is not None: + for i in range(3): + pid = f"h{h}_f{f}_young_{i}" + age = fam["minor_parents"] if i < 2 else 0 + people[pid] = {"age": age, "is_parent": i < 2} + if house["head_flags"] != "unset": + people[pid]["is_household_head"] = False + ids.append(pid) + facts["flagged"].append(False) + facts["age"].append(age) + child_age = fam["child_age"] + if f > 0 and fam["minor_age"] is not None: + child_age = fam["minor_age"] + if child_age is not None: + pid = f"h{h}_f{f}_child" + people[pid] = {"age": child_age} + if house["head_flags"] != "unset": + people[pid]["is_household_head"] = False + ids.append(pid) + facts["flagged"].append(False) + facts["age"].append(child_age) + facts["household"].extend([h] * len(ids)) + facts["benunit"].extend([benunit_index] * len(ids)) + # The first family is only a sharer if it does not hold the head. + facts["sharer"].append(fam["sharer"] and f > 0) + benunits[f"h{h}_f{f}"] = { + "members": ids, + "liable_for_share_of_household_rent": fam["sharer"] and f > 0, + "claims_all_entitled_benefits": True, + "would_claim_uc": True, + } + members.extend(ids) + benunit_index += 1 + homes[f"h{h}"] = { + "members": members, + "country": country, + "local_authority": local_authority, + "council_tax": house["council_tax"], + "rent": house["rent"], + "tenure_type": "RENT_PRIVATELY", + "savings": house["savings"], + } + situation = { + group: { + name: { + key: value if key == "members" else {YEAR: value} + for key, value in entity.items() + } + for name, entity in entities.items() + } + for group, entities in ( + ("people", people), + ("benunits", benunits), + ("households", homes), + ) + } + return situation, {k: np.array(v) for k, v in facts.items()} + + +def calc(simulation, variable, map_to=None): + return np.asarray(simulation.calculate(variable, YEAR, map_to=map_to)) + + +def household_of_benunit(facts): + """The household index of each benefit unit.""" + households = np.zeros(facts["sharer"].size, dtype=int) + households[facts["benunit"]] = facts["household"] + return households + + +def per_household(values, facts): + """Sum benefit-unit values within each household.""" + return np.bincount(household_of_benunit(facts), weights=values.astype(float)) + + +@PROPERTY_SETTINGS +@given(population) +def test_claimant_invariants(population): + situation, facts = build(population) + sim = Simulation(situation=situation) + head = calc(sim, "benunit_contains_household_head") + claimant = calc(sim, "council_tax_reduction_claimant_benunit") + sharer = facts["sharer"] + shared = per_household(sharer, facts) > 0 + n_households = shared.size + person_household = facts["household"] + person_head_family = head[facts["benunit"]] + + # 1. One head, holding a flagged member if any, else the eldest. + assert np.all(per_household(head, facts) == 1) + for h in range(n_households): + in_h = person_household == h + flagged = facts["flagged"][in_h] + if population[h]["head_flags"] in ("one", "several"): + assert np.any(flagged & person_head_family[in_h]) + elif population[h]["head_flags"] == "none" or not all( + house["head_flags"] == "unset" for house in population + ): + ages = facts["age"][in_h] + assert np.any((ages == ages.max()) & person_head_family[in_h]) + + # 2. Claimants. + claimants = per_household(claimant, facts) + assert np.all(claimants[~shared] == 1) # every head here is 18 or over + age = calc(sim, "age") + claimant_or_partner = calc(sim, "is_claimant_or_partner") + has_adult = np.zeros(claimant.size, dtype=bool) + np.logical_or.at(has_adult, facts["benunit"], age >= 18) + assert not np.any(claimant & ~has_adult) + liable = calc(sim, "council_tax_reduction_liable_person") + head_person = calc(sim, "council_tax_reduction_household_head") + in_head_or_sharer = (head | sharer)[facts["benunit"]] + assert np.all(age[liable] >= 18) + assert np.all(in_head_or_sharer[liable]) + student = calc(sim, "in_HE") + person_shared = shared[person_household] + excluded_student = person_shared & student + head_adult = np.zeros(claimant.size, dtype=bool) + np.logical_or.at( + head_adult, + facts["benunit"], + head_person & (age >= 18) & ~excluded_student, + ) + assert np.all(claimant[head_adult]) + claimants_not_shared = per_household(claimant, facts)[~shared] + assert np.all(claimants_not_shared <= 1) + has_adult_non_student = np.zeros(claimant.size, dtype=bool) + np.logical_or.at(has_adult_non_student, facts["benunit"], (age >= 18) & ~student) + benunit_shared = shared[household_of_benunit(facts)] + assert not np.any(claimant & benunit_shared & ~has_adult_non_student) + has_liable = np.zeros(claimant.size, dtype=bool) + np.logical_or.at(has_liable, facts["benunit"], liable & ~excluded_student) + assert np.array_equal(claimant, has_liable) + + # 3. Simulated reductions: none outside claimant families; never above + # the council tax. + reduction = calc(sim, "simulated_council_tax_reduction_benunit") + assert np.all(reduction[~claimant] == 0) + council_tax = calc(sim, "council_tax") + assert np.all(per_household(reduction, facts) <= council_tax + 0.01) + + # 8. Shares of the council tax sum to at most one. + share = calc(sim, "council_tax_reduction_joint_liability_share") + assert np.all(per_household(share * claimant, facts) <= 1 + 1e-6) + + # 4. Non-dependants. + non_dep = calc(sim, "council_tax_reduction_individual_non_dep_deduction_eligible") + person_claimant = claimant[facts["benunit"]] + person_sharer = sharer[facts["benunit"]] + lodger = calc(sim, "pays_rent_to_householder") + adult = age >= 18 + assert not np.any(non_dep & person_claimant & claimant_or_partner) + assert not np.any(non_dep & person_sharer) + assert np.all(non_dep[adult & ~person_claimant & ~person_sharer & ~lodger]) + + # 5. Differential with the person-level household head. Once anyone in + # the simulation has the input, everyone else's defaults to false, so an + # "unset" household is well formed only if every household is. + all_unset = all(house["head_flags"] == "unset" for house in population) + well_formed = np.array( + [ + population[h]["head_flags"] == "one" + or (population[h]["head_flags"] == "unset" and all_unset) + for h in person_household + ] + ) + person_level_head = calc(sim, "is_household_head") + assert np.array_equal( + person_head_family[well_formed], + sim.map_result( + sim.map_result(person_level_head, "person", "benunit") > 0, + "benunit", + "person", + ).astype(bool)[well_formed], + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_claimant_does_not_follow_age(population): + for house in population: + house["head_flags"] = "one" + situation, facts = build(population) + + def older_than_head(house, adult_index, age, flagged): + return age if flagged else 101 + adult_index + + raised, _ = build(population, age_override=older_than_head) + # 6. Not age. + before = calc( + Simulation(situation=situation), "council_tax_reduction_claimant_benunit" + ) + after = calc(Simulation(situation=raised), "council_tax_reduction_claimant_benunit") + assert np.array_equal(before, after) + + +@PROPERTY_SETTINGS +@given(population) +def test_no_op_where_head_is_eldest(population): + for house in population: + house["head_flags"] = "one" + for fam in house["families"]: + fam["sharer"] = False + + def head_eldest(house, adult_index, age, flagged): + return 110 if flagged else min(age, 100) + + situation, facts = build(population, age_override=head_eldest) + sim = Simulation(situation=situation) + claimant = calc(sim, "council_tax_reduction_claimant_benunit") + # 7. The previous rule: the family of the household's eldest adult. + age = calc(sim, "age") + benunit_max_age = sim.map_result( + sim.map_result(age, "person", "benunit", how="max"), "benunit", "person" + ) + household_max_age = sim.map_result( + sim.map_result(age, "person", "household", how="max"), "household", "person" + ) + previous = sim.map_result( + (benunit_max_age == household_max_age).astype(float), "person", "benunit" + ) + assert np.array_equal(claimant, previous > 0) diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 77881382c1..441cf8f460 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -344,9 +344,9 @@ def test_meals_and_council_tax(population): np.maximum(0, rent - deduction)[boarder], atol=0.01, ) - # 7. Council tax shares, where the rent is shared. (Elsewhere the family - # of the household's oldest adult claims, and families tied for oldest - # both do: a separate, existing issue.) + # 7. Council tax shares, where the rent is shared. (Elsewhere only the + # household head's family claims: test_council_tax_reduction_claimant_ + # properties.py.) claimant = calc(a, "council_tax_reduction_claimant_benunit") share = calc(a, "council_tax_reduction_joint_liability_share") total = per_household(a, claimant * share) @@ -355,7 +355,7 @@ def test_meals_and_council_tax(population): assert shared.any() assert np.all(total[shared] <= 1 + 1e-6) # Each claim there follows its own family's pensioner status, not the - # household's oldest family's (SI 2012/2885 reg 3). + # household head's family's (SI 2012/2885 reg 3). in_shared = of_household(a, "council_tax_reduction_claims_are_joint") assert np.array_equal( calc(a, "council_tax_reduction_claim_pensioner")[in_shared], diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/benunit_contains_household_head.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/benunit_contains_household_head.py index 6a2d45c076..195f8b4ea7 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/benunit_contains_household_head.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/benunit_contains_household_head.py @@ -4,12 +4,27 @@ class benunit_contains_household_head(Variable): value_type = bool entity = BenUnit - label = "Benefit unit contains the oldest adult in the household" + label = "Benefit unit contains the household head" + documentation = ( + "Whether the household head is in this family. The head is the " + "household reference person: a householder, in whose name the " + "accommodation is owned or rented. Council Tax Reduction takes the " + "head as the resident liable for the council tax under the Local " + "Government Finance Act 1992 (s.6 in England and Wales, s.75 in " + "Scotland), which every class of applicant requires. Each household " + "has exactly one head. Where the input flags more than one member, " + "the eldest flagged member is the head; where it flags none, the " + "eldest member is. Rent and non-dependant rules for Universal Credit " + "and Housing Benefit read is_household_head directly, so they agree " + "with this only where exactly one member is flagged." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/14/section/6", + "https://www.legislation.gov.uk/ukpga/1992/14/section/75", + ) def formula(benunit, period, parameters): - person = benunit.members - household = person.household - benunit_max_age = benunit.max(person("age", period)) - household_max_age = benunit.max(household.max(person("age", period))) - return benunit_max_age == household_max_age + return benunit.any( + benunit.members("council_tax_reduction_household_head", period) + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py index dc1f0a2f2f..e867966cd0 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_claimant_benunit.py @@ -7,32 +7,52 @@ class council_tax_reduction_claimant_benunit(Variable): label = "Family claims Council Tax Reduction for the household" documentation = ( "Whether this family can claim Council Tax Reduction on the " - "household's council tax. Where the household's rent is shared, every " - "family liable for it is jointly and severally liable for the council " - "tax and claims on its part, through a liable member who is not an " - "excluded full-time student. Otherwise the family of the household's " - "oldest adult claims, as before." + "household's council tax. Every class of person entitled to a " + "reduction is limited to people liable to pay the council tax, and " + "liability goes to the resident (aged 18 or over) with the greatest " + "interest in the dwelling, jointly where several hold the same " + "interest. The model takes the household head (the household " + "reference person, a householder in whose name the accommodation is " + "owned or rented) as that resident, and families liable for a share " + "of the household's rent as jointly liable with the head's family. " + "Other families are treated as not liable, whatever their ages. A " + "family claims only through a member treated as liable, who must be " + "aged 18 or over: see council_tax_reduction_liable_person. Where the " + "household's rent is shared, that member must also not be an " + "excluded full-time student. " + "Joint owners in separate families, and dwellings where the owner " + "rather than a resident is liable, are not identified." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/ukpga/1992/14/section/6", - "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", + "https://www.legislation.gov.uk/ukpga/1992/14/section/13A", + "https://www.legislation.gov.uk/ukpga/1992/14/section/75", + "https://www.legislation.gov.uk/ukpga/1992/14/section/99", + "https://www.legislation.gov.uk/ukpga/1992/14/schedule/1A/paragraph/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/16", "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/75", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/21", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/22", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/23", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/24", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/25", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/13", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/14", ) def formula(benunit, period, parameters): person = benunit.members - rent_is_shared = benunit.household( + # The head's family and families liable for a share of the rent claim, + # each through a member treated as liable (aged 18 or over). + liable = person("council_tax_reduction_liable_person", period) + # Where the rent is shared, a full-time student is excluded from + # entitlement (Default Scheme Sch para 75(1)); the model takes a + # person in higher education as one, so a family there claims through + # a liable non-student. + excluded_student = person.household( "council_tax_reduction_claims_are_joint", period - ) - # A full-time student is excluded from entitlement (Default Scheme - # Sch para 75(1)); the model takes a person in higher education as - # one, so a sharer family claims through a liable non-student. - liable_family = benunit.any( - person("is_liable_for_household_rent", period) & ~person("in_HE", period) - ) - return where( - rent_is_shared, - liable_family, - benunit("benunit_contains_household_head", period), - ) + ) & person("in_HE", period) + return benunit.any(liable & ~excluded_student) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_head.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_head.py new file mode 100644 index 0000000000..1f0229bf12 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_head.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_household_head(Variable): + value_type = bool + entity = Person + label = "Household head for Council Tax Reduction" + documentation = ( + "The member Council Tax Reduction treats as the household head: the " + "household reference person, exactly one per household. Where the " + "input flags more than one member, the eldest flagged member is the " + "head; where it flags none, the eldest member is. Ties go by order." + ) + definition_period = YEAR + + def formula(person, period, parameters): + household = person.household + age = person("age", period) + flagged = person("is_household_head", period) + # get_rank breaks ties in age by order within the household, so + # exactly one member has rank 0. + rank = where( + household.any(flagged), + person.get_rank(household, -age, condition=flagged), + person.get_rank(household, -age), + ) + return rank == 0 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py index f5baa192f3..3e34def02e 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_joint_liability_share.py @@ -11,17 +11,24 @@ class council_tax_reduction_joint_liability_share(Variable): "the maximum reduction is divided by the number of people jointly and " "severally liable, leaving out students excluded from the scheme, and " "a deduction for a non-dependant of two or more of them is apportioned " - "equally between them. The model takes the people liable for the " - "household's rent as those jointly liable for its council tax " - "(residents with the same interest, Local Government Finance Act 1992 " - "s.6), takes a person in higher education as an excluded full-time " - "student, and follows the regulations' wording, counting the " - "claimant's partner. Otherwise the share is one." + "equally between them. The people jointly liable are those the model " + "treats as liable for the council tax " + "(council_tax_reduction_liable_person: the household head, the " + "head's partner and the claimants and partners of families liable " + "for a share of the rent, each aged 18 or over; Local Government " + "Finance Act 1992 s.6, s.9, s.75, s.77 and s.77A). The model takes a " + "person in higher education as an excluded full-time student, and " + "follows the regulations' wording, counting the claimant's partner. " + "Otherwise the share is one." ) definition_period = YEAR unit = "/1" reference = ( "https://www.legislation.gov.uk/ukpga/1992/14/section/6", + "https://www.legislation.gov.uk/ukpga/1992/14/section/9", + "https://www.legislation.gov.uk/ukpga/1992/14/section/75", + "https://www.legislation.gov.uk/ukpga/1992/14/section/77", + "https://www.legislation.gov.uk/ukpga/1992/14/section/77A", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/75", @@ -33,7 +40,7 @@ def formula(benunit, period, parameters): # SI 2012/2885 Sch 1 para 7(3)-(4) and para 8(5); the Welsh and # Scottish schemes have the same wording. person = benunit.members - liable = person("is_liable_for_household_rent", period) + liable = person("council_tax_reduction_liable_person", period) liable_people = benunit.max(person.household.sum(liable)) in_family = benunit.sum(liable) jointly_with_others = (in_family > 0) & (liable_people > in_family) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_liable_person.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_liable_person.py new file mode 100644 index 0000000000..cdc697fb4a --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_liable_person.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_liable_person(Variable): + value_type = bool + entity = Person + label = "Treated as liable for the council tax in Council Tax Reduction" + documentation = ( + "Whether the model treats this person as liable to pay the household's " + "council tax. Liability goes to the resident with the greatest " + "interest in the dwelling, jointly where several hold the same " + "interest, and to that person's resident spouse, civil partner or " + "cohabiting partner. A resident is aged 18 or over. The model takes " + "these as liable, each only if aged 18 or over: " + "the household head; " + "the head's partner, where the head is a claimant or partner of their " + "family; " + "and the claimant and partner of any family liable for a share of " + "the household's rent. " + "Other adults in the head's family, such as grown-up children or a " + "young couple living in a grandparent's family, are not liable." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/14/section/6", + "https://www.legislation.gov.uk/ukpga/1992/14/section/9", + "https://www.legislation.gov.uk/ukpga/1992/14/section/75", + "https://www.legislation.gov.uk/ukpga/1992/14/section/77", + "https://www.legislation.gov.uk/ukpga/1992/14/section/77A", + "https://www.legislation.gov.uk/ukpga/1992/14/section/99", + ) + + def formula(person, period, parameters): + adult = person("age", period) >= 18 # s.6(5), s.99(1) + head = person("council_tax_reduction_household_head", period) + claimant_or_partner = person("is_claimant_or_partner", period) + head_is_claimant_or_partner = person.benunit.any(head & claimant_or_partner) + head_family = person.benunit.any(head) + heads_partner = head_family & head_is_claimant_or_partner & claimant_or_partner + sharer = person.benunit("liable_for_share_of_household_rent", period) + return adult & (head | heads_partner | (sharer & claimant_or_partner))