diff --git a/changelog.d/ctr-applicant-means-test.fixed.md b/changelog.d/ctr-applicant-means-test.fixed.md new file mode 100644 index 000000000..010dc6840 --- /dev/null +++ b/changelog.d/ctr-applicant-means-test.fixed.md @@ -0,0 +1 @@ +Means-test the Council Tax Reduction applicant and their partner rather than the benefit unit's claimant and partner, so a household head who is liable for the council tax but shares a benefit unit with a young couple is assessed on her own income, allowance, premiums and exemption. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml new file mode 100644 index 000000000..b5af6ad00 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/applicant_or_partner.yaml @@ -0,0 +1,591 @@ +# The Council Tax Reduction means test covers the applicant's own income and +# their partner's (SSI 2012/319 regs 14, 21, 23 and 26; SI 2012/2885 Sch 1 +# para 11; WSI 2013/3029 Sch 1 para 5 and Sch 6 para 7; SSI 2021/249 reg 36). +# The applicant is the person liable for the council tax. Where that is a +# household head who is not the benefit unit's claimant or partner (here a +# grandmother sharing a benefit unit with a young couple and their baby), the +# means test is the head's own, as a single person; the benefit unit's +# benefits belong to the couple. None of these cases sets applicable income. +# +# Rates for 2026 (parameters): pension-age single personal allowance +# £256/week, working-age single aged 25 or over £95.55/week, couple aged 18 +# or over £150.15/week, disability premium (single) £44.85/week, severe +# disability premium (single) £86.05/week; 52 weeks. Scotland, Wales and the +# English pensioner scheme: maximum reduction 100%, withdrawal 20%. Newham: +# maximum 70%, withdrawal 25%. Personal allowance £12,570, basic rate 20%. + +- name: A grandmother head with a young couple in her benefit unit and no income of her own gets the full reduction + 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] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + is_claimant_or_partner: [false, true, true, false] + council_tax_reduction_liable_person: [true, false, false, false] + council_tax_reduction_head_applies_alone: [true] + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + # The couple's Child Benefit for the baby is theirs, not hers. + council_tax_reduction_applicable_income: [0] + # A single pensioner: 256 * 52. + council_tax_reduction_applicable_amount: [13_312] + simulated_council_tax_reduction_benunit: [1_800] + council_tax_reduction: 1_800 + +- name: The same grandmother with a £100,000 private pension gets no reduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + private_pension_income: 100_000 + 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] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + council_tax_reduction_applicable_amount: [13_312] + # Her pension after tax is far above £13,312, and 20% of the excess is + # more than the £1,800 council tax. + simulated_council_tax_reduction_benunit: [0] + council_tax_reduction: 0 + +- name: In England a grandmother head's own £20,000 pension is withdrawn at 20% above her allowance + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + private_pension_income: 20_000 + young_mother: + age: 17 + is_parent: true + is_household_head: false + employment_income: 10_000 + 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] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_pensioner: [true] + # Her pension less income tax of (20,000 - 12,570) * 20% = 1,486. The + # young mother's earnings are not hers. + council_tax_reduction_applicable_income: [18_514] + council_tax_reduction_applicable_amount: [13_312] + # 1,800 - (18,514 - 13,312) * 20%. + simulated_council_tax_reduction_benunit: [759.60] + +- name: The young couple's income-based Jobseeker's Allowance does not passport the grandmother head + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + private_pension_income: 20_000 + young_mother: + age: 19 + is_parent: true + is_household_head: false + young_father: + age: 19 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + jsa_income: 4_000 + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + is_claimant_or_partner: [false, true, true, false] + council_tax_reduction_liable_person: [true, false, false, false] + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + council_tax_reduction_relevant_income_based_benefit: [false] + # Neither the couple's JSA nor their Child Benefit is her income. + council_tax_reduction_applicable_income: [18_514] + simulated_council_tax_reduction_benunit: [759.60] + +- name: A head who applies alone is a single person, with premiums on her own disability only + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 60 + is_household_head: true + is_disabled_for_benefits: true + # Attendance Allowance qualifies her for the severe disability premium. + attendance_allowance: 4_000 + young_mother: + age: 17 + is_parent: true + is_household_head: false + is_disabled_for_benefits: true + 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] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # Disability (44.85) and severe disability (86.05) premiums at the single + # rate: (44.85 + 86.05) * 52. The young mother's disability does not give + # the grandmother a premium, and at 17 neither parent is a non-dependant + # who bars the severe disability premium. + council_tax_reduction_premiums: [6_806.80] + # Single aged 25 or over, 95.55 * 52 = 4,968.60, plus the premiums. + council_tax_reduction_applicable_amount: [11_775.40] + +- name: Adult parents in a lone-applying head's benefit unit are her non-dependants and bar her severe disability premium + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 60 + is_household_head: true + is_disabled_for_benefits: true + # Attendance Allowance qualifies her for the severe disability premium. + attendance_allowance: 4_000 + young_mother: + age: 19 + is_parent: true + is_household_head: false + is_disabled_for_benefits: true + young_father: + age: 19 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # At 19 the parents are residents aged 18 or over outside her family, so + # the severe disability premium is not payable (HB Regs 2006 Sch 3 para + # 14(2)(a)(ii), which the model applies to Council Tax Reduction too). + # Only the disability premium remains: 44.85 * 52. + council_tax_reduction_premiums: [2_332.20] + council_tax_reduction_applicable_amount: [7_300.80] + +- name: A blind grandmother head who applies alone has no non-dependant deduction in Merton + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 60 + is_household_head: true + is_blind: 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 + son: + age: 30 + is_household_head: false + employment_income: 30_000 + weekly_hours: 40 + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + son: + members: [son] + claims_all_entitled_benefits: false + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_pensioner: [false, false] + # The exemption is the applicant's or partner's (SI 2012/2885 Sch 1 para + # 8(6); the Merton scheme follows it). + council_tax_reduction_applicant_has_non_dep_exemption: [true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false, false, true] + merton_council_tax_reduction_non_dep_deductions: [0, 0] + merton_council_tax_reduction: [1_800, 0] + +- name: The young couple's Universal Credit does not put a grandmother head on Newham's Universal Credit route + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 55 + is_household_head: true + young_mother: + age: 19 + is_parent: true + is_household_head: false + young_father: + age: 19 + is_parent: true + is_household_head: false + employment_income: 30_000 + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + universal_credit: 6_000 + child_benefit: 1_000 + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: ENGLAND + local_authority: NEWHAM + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_head_applies_alone: [true] + # The young father's earnings and the couple's award are not hers. + council_tax_reduction_applicable_income: [0] + # Single aged 25 or over: 95.55 * 52. + council_tax_reduction_applicable_amount: [4_968.60] + # No excess income: 1,800 * 70%. On the Universal Credit route the + # couple's earnings and award would leave nothing. + newham_council_tax_reduction: [1_260] + +- name: A partner under 18 is not liable, but their income counts in the couple's means test + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 19 + is_household_head: true + is_benunit_head: false + partner: + # The benefit unit's head, so the HBAI fallback does not read the + # 17-year-old as a dependent child. + age: 17 + is_household_head: false + is_benunit_head: true + employment_income: 10_000 + benunits: + couple: + members: [head, partner] + claims_all_entitled_benefits: true + would_claim_uc: false + # The model would otherwise pay Child Benefit for the 17-year-old + # partner. SI 2006/223 reg 12 bars it while a qualifying young person + # lives with a partner as a couple, unless that partner is in + # education or training; that is a separate issue. + child_benefit: 0 + households: + home: + members: [head, partner] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + is_claimant_or_partner: [true, true] + council_tax_reduction_liable_person: [true, false] + council_tax_reduction_head_applies_alone: [false] + is_council_tax_reduction_applicant_or_partner: [true, true] + # Below the personal allowance and the National Insurance threshold. + council_tax_reduction_applicable_income: [10_000] + # Couple aged 18 or over: 150.15 * 52 = 7,807.80. + council_tax_reduction_applicable_amount: [7_807.80] + # 1,800 - (10,000 - 7,807.80) * 20%. + simulated_council_tax_reduction_benunit: [1_361.56] + +- name: A head under 18 with an adult partner claims through the partner, and the head's income counts + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 17 + is_household_head: true + is_benunit_head: true + employment_income: 10_000 + partner: + age: 18 + is_household_head: false + is_benunit_head: false + benunits: + couple: + members: [head, partner] + claims_all_entitled_benefits: true + would_claim_uc: false + # The model would otherwise pay Child Benefit for the 17-year-old + # partner. SI 2006/223 reg 12 bars it while a qualifying young person + # lives with a partner as a couple, unless that partner is in + # education or training; that is a separate issue. + child_benefit: 0 + households: + home: + members: [head, partner] + country: WALES + local_authority: CARDIFF + council_tax: 1_800 + savings: 0 + output: + is_claimant_or_partner: [true, true] + council_tax_reduction_liable_person: [false, true] + is_council_tax_reduction_applicant_or_partner: [true, true] + council_tax_reduction_applicable_income: [10_000] + council_tax_reduction_applicable_amount: [7_807.80] + simulated_council_tax_reduction_benunit: [1_361.56] + +- name: A pensioner who is not the applicant or partner does not make a young couple's claim a pensioner's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 70 + is_household_head: false + mother: + age: 19 + is_parent: true + is_household_head: true + father: + age: 19 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, mother, father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [grandmother, mother, father, baby] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + savings: 0 + output: + # The mother is the head and the couple are the benefit unit's claimant + # and partner, so they are the applicant and partner. + council_tax_reduction_head_applies_alone: [false] + is_council_tax_reduction_applicant_or_partner: [false, true, true, false] + council_tax_reduction_pensioner: [false] + # A working-age couple aged 18 or over: 150.15 * 52, not the pension-age + # couple rate. + council_tax_reduction_applicable_amount: [7_807.80] + +- name: The young couple's Universal Credit does not remove a grandmother head's tariff income from capital in Kingston upon Thames + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 55 + is_household_head: true + private_pension_income: 5_000 + young_mother: + age: 19 + is_parent: true + is_household_head: false + young_father: + age: 19 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + universal_credit: 6_000 + child_benefit: 1_000 + households: + home: + members: [grandmother, young_mother, young_father, baby] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + council_tax: 1_800 + savings: 10_000 + output: + council_tax_reduction_head_applies_alone: [true] + council_tax_reduction_applicable_income: [5_000] + council_tax_reduction_applicable_amount: [4_968.60] + # Tariff income ceil((10,000 - 6,000) / 250) = £16 a week, 832 a year, + # since she has no award. 1,800 - (5,000 + 832 - 4,968.60) * 20%. + kingston_upon_thames_council_tax_reduction: [1_627.32] + +- name: A blind grandmother head's exemption does not exempt a sharer's claim from a non-dependant deduction in Merton + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 60 + is_household_head: true + is_blind: 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 + sharer: + age: 40 + is_household_head: false + son: + age: 30 + is_household_head: false + employment_income: 30_000 + weekly_hours: 40 + benunits: + family: + members: [grandmother, young_mother, young_father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + claims_all_entitled_benefits: true + would_claim_uc: false + son_family: + members: [son] + claims_all_entitled_benefits: false + would_claim_uc: false + households: + home: + members: [grandmother, young_mother, young_father, baby, sharer, son] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_liable_person: [true, false, false, false, true, false] + council_tax_reduction_joint_liability_share: [0.5, 0.5, 1] + # The grandmother applies alone and is blind, so her claim has no + # deduction. The sharer is neither her nor her partner, so the sharer's + # claim bears half the son's deduction: Merton charges £13.30 a week for + # gross weekly income of £485 or more in remunerative work (30,000 / 52 = + # 576.92), 13.30 * 52 / 2 = 345.80 (Merton scheme para 30(5)-(6)). + council_tax_reduction_applicant_has_non_dep_exemption: [true, false, false] + merton_council_tax_reduction_non_dep_deductions: [0, 345.80, 0] + # Each claim covers half the £1,800; neither has income above its + # allowance. + merton_council_tax_reduction: [900, 554.20, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml index 793efe150..005c41c6f 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml @@ -969,6 +969,7 @@ output: is_council_tax_reduction_applicant_or_partner: [true, false, false, false] council_tax_reduction_head_applies_alone: [true] - council_tax_reduction_relevant_income_based_benefit: [true] + # Nor does it passport her: the passport is the applicant's award. + council_tax_reduction_relevant_income_based_benefit: [false] council_tax_reduction_pensioner: [true] simulated_council_tax_reduction_benunit: [1_800] diff --git a/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py b/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py new file mode 100644 index 000000000..565a69e74 --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_applicant_properties.py @@ -0,0 +1,403 @@ +"""Property-based tests for whose circumstances the Council Tax Reduction +means test assesses. + +The means test covers the applicant's own income and their partner's (SSI +2012/319 regs 14, 21, 23 and 26; SI 2012/2885 Sch 1 para 11; WSI 2013/3029 +Sch 1 para 5 and Sch 6 para 7; SSI 2021/249 reg 36), and the applicant is the +person liable for the council tax. The model takes the household head as +liable (council_tax_reduction_liable_person). Usually the head is the +benefit unit's claimant or partner, but a grandmother who heads the household +can share a benefit unit with a young couple who are its claimant and +partner (#1896's parent-couple rule). She then applies alone. + +Invariants, for any generated population of households: heads alone, in a +couple (with partners aged 16 and over), or sharing a benefit unit with two +parents aged 16 to 30 and their baby; another family, sometimes a sharer; +income-based Jobseeker's Allowance and Universal Credit supplied for the +head's benefit unit; the head's disability, Attendance Allowance, Carer's +Allowance and blindness; all eight modelled schemes. + +1. Head's income counts: raising the liable household head's own private + pension by £5,000 raises their family's applicable income. Raising a + young parent's earnings by £5,000 leaves the family's applicable income + unchanged where the head applies alone. +2. Monotone: raising the head's pension never raises any family's simulated + reduction. +3. High income: with £250,000 more pension, the head's family gets no + simulated reduction, unless an income-based benefit passports it or its + applicant has a Universal Credit award (both supplied as inputs here; + those routes are outside this invariant). +4. Identity: a liable head is always an applicant or partner of their own + family. Where the head does not apply alone, the applicant and partner + are exactly the benefit unit's claimant and partner (ordinary families + are unchanged). Where the head applies alone, the head is the only one. +5. Differential: a head of any age from 18 sharing a benefit unit with + parents aged 16 or 17 and their baby has the same applicable income, + applicable amount, premiums, passport, scheme, exemption and simulated + reduction as the same head entered as her own benefit unit, with the young + family as another. The two inputs describe the same people, so the law + gives the same answer. Two limits keep the inputs comparable: no one + claims Universal Credit, Income Support or Pension Credit by take-up (a + head inside another couple's benefit unit cannot be given her own award), + and the parents are under 18 (older ones would be her non-dependants, + which the model charges a deduction for only when they are in another + benefit unit). +""" + +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"), +] +SHAPES = ["alone", "couple", "young_parents"] +# The head's own circumstances that the premiums and the exemption read: +# disability, Attendance Allowance (a severe disability premium qualifying +# benefit, which also exempts her from non-dependant deductions), Carer's +# Allowance (the carer premium) and blindness. +head_circumstances = st.fixed_dictionaries( + dict( + is_disabled_for_benefits=st.booleans(), + is_enhanced_disabled_for_benefits=st.booleans(), + attendance_allowance=st.sampled_from([0, 0, 4_000]), + carers_allowance=st.sampled_from([0, 0, 4_000]), + is_blind=st.booleans(), + ) +) +money = st.floats(0, 40_000, allow_nan=False, allow_infinity=False) +# Incomes, bills and savings around the means-test thresholds, so awards are +# often part-withdrawn rather than nil or full in both representations. +pension = st.one_of(st.sampled_from([0, 5_000, 8_000, 12_000, 20_000]), money) +council_tax = st.one_of(st.just(1_800), st.floats(0, 4_000, allow_nan=False)) +savings = st.one_of(st.just(0), st.floats(0, 20_000, allow_nan=False)) +head_age = st.one_of( + st.sampled_from([18, 25, 40, 60, 66, 68, 80]), st.integers(18, 100) +) +PENSION_STEP = 5_000 +HIGH_PENSION = 250_000 + + +@st.composite +def households(draw): + return dict( + scheme=draw(st.sampled_from(SCHEMES)), + shape=draw(st.sampled_from(SHAPES)), + head_age=draw(head_age), + head_pension=draw(pension), + head_flags=draw(head_circumstances), + partner_age=draw(st.integers(16, 100)), + parent_age=draw(st.integers(16, 30)), + other_earnings=draw(st.lists(money, min_size=2, max_size=2)), + jsa_income=draw(st.sampled_from([None, None, 3_000])), + universal_credit=draw(st.sampled_from([None, None, 5_000])), + other_family=draw(st.sampled_from([None, "non_dependant", "sharer"])), + other_age=draw(st.integers(16, 90)), + council_tax=draw(council_tax), + savings=draw(savings), + ) + + +def person_input(values): + return {key: {YEAR: value} for key, value in values.items()} + + +def add_household(people, benunits, homes, name, house, variant): + """Add one household. variant: "base", "pension", "high_pension" or + "parent_earnings" (the young mother's earnings raised).""" + country, local_authority = house["scheme"] + pension = house["head_pension"] + { + "pension": PENSION_STEP, + "high_pension": HIGH_PENSION, + }.get(variant, 0) + head = f"{name}_head" + people[head] = person_input( + dict( + age=house["head_age"], + is_household_head=True, + private_pension_income=pension, + **house["head_flags"], + ) + ) + family = [head] + if house["shape"] == "couple": + people[f"{name}_partner"] = person_input( + dict( + age=house["partner_age"], + is_household_head=False, + employment_income=house["other_earnings"][0], + ) + ) + family.append(f"{name}_partner") + elif house["shape"] == "young_parents": + for i, parent in enumerate(["mother", "father"]): + earnings = house["other_earnings"][i] + if variant == "parent_earnings" and parent == "mother": + earnings += PENSION_STEP + people[f"{name}_{parent}"] = person_input( + dict( + age=house["parent_age"], + is_parent=True, + is_household_head=False, + employment_income=earnings, + ) + ) + family.append(f"{name}_{parent}") + people[f"{name}_baby"] = person_input(dict(age=0, is_household_head=False)) + family.append(f"{name}_baby") + head_benunit = dict( + members=family, + claims_all_entitled_benefits={YEAR: True}, + would_claim_uc={YEAR: False}, + ) + for benefit in ["jsa_income", "universal_credit"]: + if house[benefit] is not None: + head_benunit[benefit] = {YEAR: house[benefit]} + benunits[f"{name}_family"] = head_benunit + members = list(family) + if house["other_family"] is not None: + other = f"{name}_other" + people[other] = person_input( + dict( + age=house["other_age"], + is_household_head=False, + employment_income=house["other_earnings"][1], + ) + ) + benunits[f"{name}_other_family"] = dict( + members=[other], + claims_all_entitled_benefits={YEAR: True}, + would_claim_uc={YEAR: False}, + liable_for_share_of_household_rent={ + YEAR: house["other_family"] == "sharer" + }, + ) + members.append(other) + homes[name] = person_input( + dict( + country=country, + local_authority=local_authority, + council_tax=house["council_tax"], + savings=house["savings"], + ) + ) + homes[name]["members"] = members + + +VARIANTS = ["base", "pension", "high_pension", "parent_earnings"] + + +def simulate(population): + """One simulation holding every household in every variant.""" + people, benunits, homes = {}, {}, {} + for h, house in enumerate(population): + for variant in VARIANTS: + add_household(people, benunits, homes, f"h{h}_{variant}", house, variant) + return Simulation( + situation=dict(people=people, benunits=benunits, households=homes) + ) + + +def by_benunit(sim, variable, names): + """A benefit-unit variable for the named benefit units.""" + values = sim.calculate(variable, YEAR) + ids = list(sim.populations["benunit"].ids) + return np.array([values[ids.index(name)] for name in names]) + + +@given(st.lists(households(), min_size=1, max_size=6)) +@PROPERTY_SETTINGS +def test_council_tax_reduction_assesses_the_liable_head(population): + sim = simulate(population) + head_family = { + variant: [f"h{h}_{variant}_family" for h in range(len(population))] + for variant in VARIANTS + } + + def family(variable, variant): + return by_benunit(sim, variable, head_family[variant]) + + head_alone = family("council_tax_reduction_head_applies_alone", "base") + + # 1. The head's pension counts; a young parent's earnings do not where + # the head applies alone. + income = family("council_tax_reduction_applicable_income", "base") + assert (family("council_tax_reduction_applicable_income", "pension") > income).all() + parent_income = family("council_tax_reduction_applicable_income", "parent_earnings") + assert np.allclose(parent_income[head_alone], income[head_alone]) + + # 2. Monotone in the head's pension, for every family. + all_ids = list(sim.populations["benunit"].ids) + simulated = sim.calculate("simulated_council_tax_reduction_benunit", YEAR) + + def every_family(variant): + return np.array( + [simulated[all_ids.index(name)] for name in _all_names(population, variant)] + ) + + base_ctr = every_family("base") + for variant in ["pension", "high_pension"]: + assert (every_family(variant) <= base_ctr + 1e-6).all() + + # 3. No reduction on a very high income, outside the passport and UC + # routes. + passported = family("council_tax_reduction_relevant_income_based_benefit", "base") + universal_credit = family("universal_credit", "base") > 0 + high = family("simulated_council_tax_reduction_benunit", "high_pension") + assessed_on_income = ~passported & ~(universal_credit & ~head_alone) + assert (high[assessed_on_income] == 0).all() + + # 4. Identity. + liable = sim.calculate("council_tax_reduction_liable_person", YEAR) + head = sim.calculate("is_resolved_household_head", YEAR) + applicant = sim.calculate("is_council_tax_reduction_applicant_or_partner", YEAR) + claimant_or_partner = sim.calculate("is_claimant_or_partner", YEAR) + alone = sim.populations["person"].benunit( + "council_tax_reduction_head_applies_alone", YEAR + ) + assert (applicant[liable & head]).all() + assert (applicant[~alone] == claimant_or_partner[~alone]).all() + assert (applicant[alone] == (liable & head)[alone]).all() + + +def _all_names(population, variant): + """Every benefit unit of every household in one variant, in order.""" + names = [] + for h, house in enumerate(population): + names.append(f"h{h}_{variant}_family") + if house["other_family"] is not None: + names.append(f"h{h}_{variant}_other_family") + return names + + +@st.composite +def grandmother_households(draw): + return dict( + scheme=draw(st.sampled_from(SCHEMES)), + head_age=draw(head_age), + head_pension=draw(pension), + head_flags=draw(head_circumstances), + parent_age=draw(st.integers(16, 17)), + parent_earnings=draw(st.lists(money, min_size=2, max_size=2)), + jsa_income=draw(st.sampled_from([None, 3_000])), + universal_credit=draw(st.sampled_from([None, 5_000])), + council_tax=draw(council_tax), + savings=draw(savings), + ) + + +def add_grandmother_household(people, benunits, homes, name, house, split): + country, local_authority = house["scheme"] + head = f"{name}_grandmother" + people[head] = person_input( + dict( + age=house["head_age"], + is_household_head=True, + private_pension_income=house["head_pension"], + **house["head_flags"], + ) + ) + young = [] + for i, parent in enumerate(["mother", "father"]): + people[f"{name}_{parent}"] = person_input( + dict( + age=house["parent_age"], + is_parent=True, + is_household_head=False, + employment_income=house["parent_earnings"][i], + ) + ) + young.append(f"{name}_{parent}") + people[f"{name}_baby"] = person_input(dict(age=0, is_household_head=False)) + young.append(f"{name}_baby") + young_benefits = { + benefit: {YEAR: house[benefit]} + for benefit in ["jsa_income", "universal_credit"] + if house[benefit] is not None + } + common = dict( + claims_all_entitled_benefits={YEAR: True}, + would_claim_uc={YEAR: False}, + would_claim_IS={YEAR: False}, + would_claim_pc={YEAR: False}, + ) + if split: + benunits[f"{name}_family"] = dict(members=[head], **common) + benunits[f"{name}_young"] = dict(members=young, **common, **young_benefits) + else: + benunits[f"{name}_family"] = dict( + members=[head] + young, **common, **young_benefits + ) + homes[name] = person_input( + dict( + country=country, + local_authority=local_authority, + council_tax=house["council_tax"], + savings=house["savings"], + ) + ) + homes[name]["members"] = [head] + young + + +DIFFERENTIAL_VARIABLES = [ + "council_tax_reduction_applicable_income", + "council_tax_reduction_applicable_amount", + "council_tax_reduction_premiums", + "council_tax_reduction_relevant_income_based_benefit", + "council_tax_reduction_pensioner", + "council_tax_reduction_applicant_has_non_dep_exemption", + "simulated_council_tax_reduction_benunit", +] + + +@given(st.lists(grandmother_households(), min_size=1, max_size=6)) +@PROPERTY_SETTINGS +def test_head_applying_alone_matches_her_own_benefit_unit(population): + people, benunits, homes = {}, {}, {} + for h, house in enumerate(population): + for split in [False, True]: + add_grandmother_household( + people, + benunits, + homes, + f"h{h}_{'split' if split else 'merged'}", + house, + split, + ) + sim = Simulation(situation=dict(people=people, benunits=benunits, households=homes)) + merged = [f"h{h}_merged_family" for h in range(len(population))] + split = [f"h{h}_split_family" for h in range(len(population))] + assert by_benunit(sim, "council_tax_reduction_head_applies_alone", merged).all() + assert not by_benunit(sim, "council_tax_reduction_head_applies_alone", split).any() + for variable in DIFFERENTIAL_VARIABLES: + assert np.allclose( + by_benunit(sim, variable, merged), by_benunit(sim, variable, split) + ), variable + household_ids = list(sim.populations["household"].ids) + for variable in [ + "council_tax_reduction", + ]: + values = sim.calculate(variable, YEAR) + merged_values = [ + values[household_ids.index(f"h{h}_merged")] for h in range(len(population)) + ] + split_values = [ + values[household_ids.index(f"h{h}_split")] for h in range(len(population)) + ] + assert np.allclose(merged_values, split_values), variable diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py index 8f8d97ac9..52ec75c7d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py @@ -44,44 +44,9 @@ class housing_benefit_net_earnings(Variable): ) def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.housing_benefit.means_test - person = benunit.members - employment_income = person("employment_income", period) - self_employment_income = person("self_employment_income", period) - # Statutory sick and maternity pay are earnings (reg 35(1)(i); pension - # age reg 35(1)(h)). The model taxes them as employment income - # (employment_benefits) but leaves them out of total_income. - statutory_pay = person("employment_benefits", period) - earnings = ( - max_(employment_income, 0) + max_(self_employment_income, 0) + statutory_pay - ) - # Income tax is attributed by the earnings' share of total income, - # with statutory pay added and before any negative component (a - # loss), so that a loss set against other income does not drop or - # inflate the share of tax on these earnings. - income_before_losses = person("total_income", period) + statutory_pay - system = person.simulation.tax_benefit_system - for component in system.get_variable("total_income").adds: - income_before_losses += max_(-person(component, period), 0) - earnings_share = np.divide( - earnings, - income_before_losses, - out=np.zeros_like(earnings), - where=income_before_losses > 0, - ) - income_tax = person("income_tax", period) * min_(earnings_share, 1) - national_insurance = add( - person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] - ) - pension_contributions = ( - person("pension_contributions", period) - * p.pension_contribution_deduction_rate - ) - net_earnings = max_( - earnings - income_tax - national_insurance - pension_contributions, - 0, - ) # The claimant and partner: a child's or young person's earnings are # not the claimant's (reg 25(3); pension age reg 23(3)). + person = benunit.members + net_earnings = person("housing_benefit_person_net_earnings", period) claimant_or_partner = person("is_claimant_or_partner", period) return benunit.sum(net_earnings * claimant_or_partner) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_person_net_earnings.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_person_net_earnings.py new file mode 100644 index 000000000..a08c38d48 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_person_net_earnings.py @@ -0,0 +1,84 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_person_net_earnings(Variable): + value_type = float + entity = Person + label = "Net earnings for the Housing Benefit and Council Tax Reduction means tests" + documentation = ( + "A person's net earnings for the Housing Benefit and Council Tax " + "Reduction means tests: employment " + "income plus self-employment profit (each floored at zero, as a loss " + "in one employment is not set against another) plus statutory sick " + "and maternity pay (regulation 35(1)(i); pension age regulation " + "35(1)(h)), less income tax, " + "Class 1, 2 and 4 National Insurance, and half of pension " + "contributions, floored at zero. For an employee the regulations " + "deduct the income tax deducted from the earnings (regulation 36(3); " + "pension age regulation 36(2)); for the self-employed, a notional " + "basic-rate tax on the profit alone less personal reliefs (regulation " + "39(1); pension age regulation 40(1)). PolicyEngine attributes a " + "person's income tax to earnings in proportion to their share of the " + "person's total income plus statutory pay, before any negative income " + "component. " + "For employment income that is a lower bound " + "on the tax on the earnings taxed as the top slice of income; for " + "self-employment profit alongside other income it can exceed the " + "notional tax. It is exact for an employee whose earnings are their " + "only taxable income, and whenever no income tax is due. The earnings " + "of a child or young person are not the claimant's (regulation 25(3); " + "pension age regulation 23(3)): housing_benefit_net_earnings sums " + "this over the claimant and partner, and Council Tax Reduction over " + "its applicant and partner (council_tax_reduction_net_earnings)." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/35", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/36", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/38", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/39", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/35", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/36", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/39", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/40", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.housing_benefit.means_test + employment_income = person("employment_income", period) + self_employment_income = person("self_employment_income", period) + # Statutory sick and maternity pay are earnings (reg 35(1)(i); pension + # age reg 35(1)(h)). The model taxes them as employment income + # (employment_benefits) but leaves them out of total_income. + statutory_pay = person("employment_benefits", period) + earnings = ( + max_(employment_income, 0) + max_(self_employment_income, 0) + statutory_pay + ) + # Income tax is attributed by the earnings' share of total income, + # with statutory pay added and before any negative component (a + # loss), so that a loss set against other income does not drop or + # inflate the share of tax on these earnings. + income_before_losses = person("total_income", period) + statutory_pay + system = person.simulation.tax_benefit_system + for component in system.get_variable("total_income").adds: + income_before_losses += max_(-person(component, period), 0) + earnings_share = np.divide( + earnings, + income_before_losses, + out=np.zeros_like(earnings), + where=income_before_losses > 0, + ) + income_tax = person("income_tax", period) * min_(earnings_share, 1) + national_insurance = add( + person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] + ) + pension_contributions = ( + person("pension_contributions", period) + * p.pension_contribution_deduction_rate + ) + net_earnings = max_( + earnings - income_tax - national_insurance - pension_contributions, + 0, + ) + return net_earnings 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 db0d6ba79..52524b282 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 @@ -10,6 +10,15 @@ def is_full_time_student_non_dep(person, period): ) +def applicant_has_universal_credit(benunit, period): + """Whether the Council Tax Reduction applicant has the benefit unit's + Universal Credit award. The award belongs to the benefit unit's claimant + and partner, so not to a household head who applies alone.""" + return (benunit("universal_credit", period) > 0) & ~benunit( + "council_tax_reduction_head_applies_alone", period + ) + + def legacy_council_tax_reduction( benunit, period, @@ -25,7 +34,7 @@ def legacy_council_tax_reduction( applicable_amount = benunit("council_tax_reduction_applicable_amount", period) applicable_income = benunit("council_tax_reduction_applicable_income", period) universal_credit = benunit("universal_credit", period) - has_uc_award = universal_credit > 0 + has_uc_award = applicant_has_universal_credit(benunit, period) uc_applicable_amount = benunit("uc_maximum_amount", period) uc_applicable_income = ( benunit("uc_earned_income", period) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py index a63c5c403..01aa8ec3b 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py @@ -5,6 +5,15 @@ class council_tax_reduction_applicable_amount(Variable): value_type = float entity = BenUnit label = "applicable Council Tax Reduction amount" + documentation = ( + "The applicant's personal allowance and premiums. Ages, couple status " + "and lone-parent status are those of the Council Tax Reduction " + "applicant and partner (is_council_tax_reduction_applicant_or_partner). " + "Where the household head applies alone " + "(council_tax_reduction_head_applies_alone), the head is a single " + "person with no partner or children, whatever the benefit unit's " + "claimant and partner are. Premiums are council_tax_reduction_premiums." + ) definition_period = YEAR unit = GBP reference = ( @@ -23,7 +32,13 @@ def formula(benunit, period, parameters): # Wales and Scotland also have separate pensioner allowance tables. # The shared definition includes qualifying age and award exclusions. pensioner = benunit("council_tax_reduction_pensioner", period) - eldest_age = benunit("eldest_claimant_or_partner_age", period) + # The applicant's and partner's ages. A household head who applies alone + # (council_tax_reduction_head_applies_alone) is a single person with no + # partner or children: the benefit unit's are its claimant's. + person = benunit.members + applicant = person("is_council_tax_reduction_applicant_or_partner", period) + head_applies_alone = benunit("council_tax_reduction_head_applies_alone", period) + eldest_age = benunit.max(where(applicant, person("age", period), -np.inf)) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold @@ -33,8 +48,10 @@ def formula(benunit, period, parameters): # Each scheme's own young person decides who is a lone parent: the # Universal Credit definition in Scotland's working-age scheme (SSI # 2021/249 regs 4 and 6), the Child Benefit definition elsewhere. - couple = benunit("is_couple", period) - lone_parent = benunit("is_council_tax_reduction_lone_parent", period) + couple = ~head_applies_alone & benunit("is_couple", period) + lone_parent = ~head_applies_alone & benunit( + "is_council_tax_reduction_lone_parent", period + ) single = ~couple & ~lone_parent single_personal_allowance = ( u_25 * p.single.younger + o_25 * p.single.older + pensioner * p.single.aged @@ -54,9 +71,5 @@ def formula(benunit, period, parameters): ) * WEEKS_IN_YEAR # The pension-age schedules have only the severe disability and carer # premiums; the same pensioner test chooses the schedule. - premiums = where( - pensioner, - benunit("pension_age_benefits_premiums", period), - benunit("working_age_benefits_premiums", period), - ) + premiums = benunit("council_tax_reduction_premiums", period) return personal_allowance + premiums diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 55575e198..d885979ec 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -14,7 +14,16 @@ class council_tax_reduction_applicable_income(Variable): "Otherwise a pension-age family's net earnings are reduced by the " "pensioner earnings disregards. The other adjustments those " "provisions allow (childcare charges, the £20 earnings disregards, " - "maintenance disregards and the rest) are not modelled." + "maintenance disregards and the rest) are not modelled. " + "The income counted is the Council Tax Reduction applicant's and " + "partner's (is_council_tax_reduction_applicant_or_partner), with, as " + "the model did before, the income of the family's children or young " + "persons. Where the household head applies alone " + "(council_tax_reduction_head_applies_alone), the benefit unit's own " + "awards (Child Benefit, Income Support, income-based JSA, " + "income-related ESA, Universal Credit, tax credits, Pension Credit) " + "and its children belong to its claimant and partner, not to the " + "head, so none of them counts." ) definition_period = YEAR unit = GBP @@ -30,28 +39,28 @@ class council_tax_reduction_applicable_income(Variable): "https://www.legislation.gov.uk/uksi/2012/2885/schedule/4", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/3", "https://www.legislation.gov.uk/ssi/2012/319/schedule/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/11", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/21", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/5", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/7", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/36", ] def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (CTR (Prescribed Requirements) (England) - # Regs 2012 Sch 1 para 11); dropping dependants' own income is a follow-up. - # Anyone else in the benefit unit does not count. + # Members whose income counts: the applicant and partner and, as the model + # did before, the programme's own children or young persons. The regulations + # count only the applicant's and partner's (CTR (Prescribed Requirements) + # (England) Regs 2012 Sch 1 para 11; SSI 2012/319 reg 21 and SSI 2021/249 + # reg 36, which also exclude a child's); dropping dependants' own income is + # a follow-up. Anyone else in the benefit unit does not count. A household + # head who applies alone is the applicant, whatever else the benefit unit + # reads her as, and the benefit unit's children are its claimant's. person = benunit.members - # A household head who applies alone - # (council_tax_reduction_head_applies_alone) is not the benefit unit's - # claimant, so this mask has reached them only as a Child Benefit young - # person. The applicant's own income always counts (SSI 2021/249 reg - # 36(1)), so they stay counted where Scotland's working-age scheme no - # longer reads them as a young person. - applicant_counted_as_young_person = person( - "is_council_tax_reduction_applicant_or_partner", period - ) & person("is_child_or_young_person_for_legacy_benefits", period) - members = ( - person("is_claimant_or_partner", period) - | person("is_child_or_young_person_for_council_tax_reduction", period) - | applicant_counted_as_young_person + head_applies_alone = benunit("council_tax_reduction_head_applies_alone", period) + applicant_family = ~head_applies_alone + members = person("is_council_tax_reduction_applicant_or_partner", period) | ( + person("is_child_or_young_person_for_council_tax_reduction", period) + & benunit.project(applicant_family) ) # Only the claimant's and partner's income-related awards: the scheme # counts the applicant's and partner's income (Sch 1 para 11 above). A @@ -97,14 +106,15 @@ def formula(benunit, period, parameters): "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income - benefits = add_for_members( - benunit, period, benunit_means_tested_benefits, members + # The benefit unit's awards are its claimant's and partner's. + benefits = applicant_family * add( + benunit, period, benunit_means_tested_benefits ) income = add_for_members(benunit, period, income_components, members) personal_benefit_income = add_for_members( benunit, period, personal_benefits, members ) - credits = add_for_members(benunit, period, ["tax_credits"], members) + credits = applicant_family * add(benunit, period, ["tax_credits"]) increased_income = income + personal_benefit_income + credits + benefits if not bi.interactions.include_in_means_tests: @@ -135,10 +145,11 @@ def formula(benunit, period, parameters): # to take account of the savings credit payable. The Pension Credit # paid on a savings-credit-only award is that savings credit (under the # Pension Credit freeze, the frozen amount). - in_receipt_of_guarantee_credit = benunit( + # The benefit unit's Pension Credit is its claimant's and partner's. + in_receipt_of_guarantee_credit = applicant_family & benunit( "in_receipt_of_guarantee_credit", period ) - has_savings_credit_only_award = benunit( + has_savings_credit_only_award = applicant_family & benunit( "in_receipt_of_savings_credit_only", period ) savings_credit_only_income = benunit("pension_credit_income", period) + benunit( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py index 16b63ab2a..46ef19a7d 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py @@ -31,10 +31,14 @@ class council_tax_reduction_assessable_capital(Variable): ] def formula(benunit, period, parameters): - in_receipt_of_guarantee_credit = benunit( + # The benefit unit's Pension Credit is its claimant's and partner's, not + # a household head's who applies alone + # (council_tax_reduction_head_applies_alone). + applicant_family = ~benunit("council_tax_reduction_head_applies_alone", period) + in_receipt_of_guarantee_credit = applicant_family & benunit( "in_receipt_of_guarantee_credit", period ) - has_savings_credit_only_award = benunit( + has_savings_credit_only_award = applicant_family & benunit( "in_receipt_of_savings_credit_only", period ) return select( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py new file mode 100644 index 000000000..193f9cbb5 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_net_earnings(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction net earnings" + documentation = ( + "Net earnings of the Council Tax Reduction applicant and partner " + "(is_council_tax_reduction_applicant_or_partner), from which the " + "pensioner earnings disregards are taken. Each person's net earnings " + "are those of the Housing Benefit means test " + "(housing_benefit_person_net_earnings), whose deductions the " + "pensioner schemes share. Where the applicant is the benefit unit's " + "claimant this equals housing_benefit_net_earnings; where the " + "household head applies alone it is the head's own net earnings." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/11", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/19", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", + ) + + def formula(benunit, period, parameters): + person = benunit.members + net_earnings = person("housing_benefit_person_net_earnings", period) + applicant_or_partner = person( + "is_council_tax_reduction_applicant_or_partner", period + ) + return benunit.sum(net_earnings * applicant_or_partner) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py index 17e26734c..a4befac40 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py @@ -61,12 +61,19 @@ def national(amount): default=england[amount], ) - net_earnings = benunit("housing_benefit_net_earnings", period) + # The applicant's and partner's earnings. A household head who applies + # alone (council_tax_reduction_head_applies_alone) is a single person + # with no partner or children. + net_earnings = benunit("council_tax_reduction_net_earnings", period) + head_applies_alone = benunit("council_tax_reduction_head_applies_alone", period) # SI 2012/2885 Sch 4 paras 2 and 8; WSI 2013/3029 Sch 3 paras 2 and 8; # SSI 2012/319 Sch 2 paras 2 and 8. A lone parent has £25 in all, # not £25 plus para 8's £5, as for Housing Benefit. weekly_amount = select( - [benunit("is_lone_parent", period), benunit("is_couple", period)], + [ + ~head_applies_alone & benunit("is_lone_parent", period), + ~head_applies_alone & benunit("is_couple", period), + ], [national("lone_parent"), national("couple")], default=national("single"), ) @@ -77,11 +84,39 @@ def national(amount): covers_additional = np.round(net_earnings.astype(float), 2) >= np.round( (standard + additional_amount).astype(float), 2 ) - additional = where( + # A head who applies alone has no partner or child, so only the + # conditions on her own age, hours and disability can apply (HB (SPC) + # Regs 2006 Sch 4 para 9(2)(b)(i) and (iv), which these schemes share). + hb_disregard = parameters( + period + ).gov.dwp.housing_benefit.means_test.income_disregard + person = benunit.members + head = person( + "is_council_tax_reduction_applicant_or_partner", period + ) & benunit.project(head_applies_alone) + hours = person("weekly_hours", period) + head_meets_conditions = benunit.any( + head + & ( + ( + (hours >= hb_disregard.worker_hours) + & (person("age", period) >= hb_disregard.worker_age) + ) + | ( + (hours >= hb_disregard.worker_hours_lower) + & person("is_disabled_for_benefits", period) + ) + ) + ) + meets_conditions = where( + head_applies_alone, + head_meets_conditions, benunit( "meets_housing_benefit_additional_earnings_disregard_conditions", period - ) - & covers_additional, + ), + ) + additional = where( + meets_conditions & covers_additional, additional_amount, 0, ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_premiums.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_premiums.py new file mode 100644 index 000000000..7cec1e159 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_premiums.py @@ -0,0 +1,86 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_premiums(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction disability and carer premiums" + documentation = ( + "The disability and carer premiums in the Council Tax Reduction " + "applicable amount, which are the applicant's. A pensioner " + "(council_tax_reduction_pensioner) has the pension-age schedule (the " + "severe disability and carer premiums), anyone else the working-age " + "schedule. Where the applicant is the benefit unit's claimant, these " + "are the benefit unit's premiums (pension_age_benefits_premiums or " + "working_age_benefits_premiums). Where the household head applies " + "alone (council_tax_reduction_head_applies_alone), they are the " + "premiums of a single claimant on the head's own circumstances, under " + "the same conditions: the disability and enhanced disability premiums " + "on her own disability (working age only); the severe disability " + "premium if she receives a qualifying benefit, no carer benefit is " + "paid for caring for her, and no other resident aged 18 or over is " + "her non-dependant (everyone else in the household, since she has no " + "partner or children, unless they receive a qualifying benefit or are " + "blind); and the carer premium if she is entitled to a carer benefit." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/6", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/2", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/20", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/1", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3", + ) + + def formula(benunit, period, parameters): + pensioner = benunit("council_tax_reduction_pensioner", period) + benefit_unit_premiums = where( + pensioner, + benunit("pension_age_benefits_premiums", period), + benunit("working_age_benefits_premiums", period), + ) + head_applies_alone = benunit("council_tax_reduction_head_applies_alone", period) + person = benunit.members + head = person( + "is_council_tax_reduction_applicant_or_partner", period + ) & benunit.project(head_applies_alone) + + def head_has(condition): + return benunit.any(head & condition) + + disability = parameters(period).gov.dwp.disability_premia + carer = parameters(period).gov.dwp.carer_premium + # The severe disability premium's non-dependants: every resident aged + # 18 or over outside the applicant's family who neither receives a + # qualifying benefit nor is blind (has_non_dependant_for_severe_ + # disability_premium). The head's family is the head alone. + counted = ( + (person("age", period) >= disability.severe_non_dependant_age) + & ~person("receives_severe_disability_premium_qualifying_benefit", period) + & ~person("is_blind", period) + ) + others_counted = benunit.max(person.household.sum(counted)) - benunit.sum( + counted & head + ) + severe = ( + head_has( + person("receives_severe_disability_premium_qualifying_benefit", period) + & ~person("is_cared_for_by_carer_benefit_recipient", period) + ) + & (others_counted == 0) + ) * disability.severe_single + carer_premium = ( + head_has(person("is_entitled_to_carer_benefit", period)) * carer.single + ) + # The pension-age schedules have no adult disability or enhanced + # disability premium. + disability_premiums = ~pensioner * ( + head_has(person("is_disabled_for_benefits", period)) + * disability.disability_single + + head_has(person("is_enhanced_disabled_for_benefits", period)) + * disability.enhanced_single + ) + head_premiums = (disability_premiums + severe + carer_premium) * WEEKS_IN_YEAR + return where(head_applies_alone, head_premiums, benefit_unit_premiums) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py index cb9300b1c..317fecffc 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py @@ -11,13 +11,17 @@ class council_tax_reduction_relevant_income_based_benefit(Variable): "tax reduction schemes to their maximum reduction. Only the " "applicant's and partner's awards count: another member of the " "benefit unit, such as a non-dependent adult, who has an award of " - "their own claims in their own right." + "their own claims in their own right. Where the household head " + "applies alone (council_tax_reduction_head_applies_alone), the " + "benefit unit's awards belong to its claimant and partner, not to the " + "head, so they do not passport her." ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2012/2886/schedule" def formula(benunit, period, parameters): - return ( + head_applies_alone = benunit("council_tax_reduction_head_applies_alone", period) + return ~head_applies_alone & ( add( benunit, period, diff --git a/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py index 46afbc7d8..0a37929f9 100644 --- a/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * import numpy as np from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + applicant_has_universal_credit, legacy_council_tax_reduction, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( @@ -25,7 +26,7 @@ def formula(benunit, period, parameters): household("country", period), benunit("council_tax_reduction_pensioner", period), ) - has_uc_award = benunit("universal_credit", period) > 0 + has_uc_award = applicant_has_universal_credit(benunit, period) capital = household("savings", period) weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold) diff --git a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py index 208dab392..274ffa526 100644 --- a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py @@ -1,5 +1,8 @@ from policyengine_uk.model_api import * import numpy as np +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + applicant_has_universal_credit, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_oxford_working_age, ) @@ -24,8 +27,7 @@ def formula(benunit, period, parameters): "council_tax_reduction_claimant_benunit", period ) would_claim = benunit("would_claim_council_tax_reduction", period) - universal_credit = benunit("universal_credit", period) - has_uc_award = universal_credit > 0 + has_uc_award = applicant_has_universal_credit(benunit, period) capital = household("savings", period) capital_eligible = capital <= ctr.means_test.capital_limit @@ -35,7 +37,11 @@ def formula(benunit, period, parameters): ) annual_income = benunit("council_tax_reduction_applicable_income", period) - child_benefit = benunit("child_benefit", period) + # Applicable income counts Child Benefit only where the applicant is + # the benefit unit's claimant, so only then is it taken off again. + child_benefit = benunit("child_benefit", period) * ~benunit( + "council_tax_reduction_head_applies_alone", period + ) weekly_income = max_(0, annual_income - child_benefit) / WEEKS_IN_YEAR weekly_income += where(has_uc_award, 0, weekly_tariff_income) relevant_income_based_benefit = benunit( diff --git a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py index eaad01138..6eaa8c1d7 100644 --- a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * import numpy as np from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + applicant_has_universal_credit, legacy_council_tax_reduction, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( @@ -23,7 +24,7 @@ def formula(benunit, period, parameters): household("country", period), benunit("council_tax_reduction_pensioner", period), ) - has_uc_award = benunit("universal_credit", period) > 0 + has_uc_award = applicant_has_universal_credit(benunit, period) capital = household("savings", period) weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold)