diff --git a/changelog.d/generic-child-adult-flags.added.md b/changelog.d/generic-child-adult-flags.added.md new file mode 100644 index 0000000000..7d0f6ccaf7 --- /dev/null +++ b/changelog.d/generic-child-adult-flags.added.md @@ -0,0 +1 @@ +- Added HBAI population variables (`is_hbai_dependent_child`, `is_hbai_adult`, `is_hbai_working_age_adult`, `is_hbai_pensioner`, `hbai_person_type`, and the equivalence-scale child bands) following the HBAI FYE 2025 glossary, `is_claimant_or_partner` for the single adult or couple a benefit unit is formed around, and programme-named child, young person and responsibility variables for Universal Credit, tax credits, the legacy means-tested benefits, the WTC childcare element and student support. diff --git a/changelog.d/generic-child-adult-flags.changed.md b/changelog.d/generic-child-adult-flags.changed.md new file mode 100644 index 0000000000..121660fc9a --- /dev/null +++ b/changelog.d/generic-child-adult-flags.changed.md @@ -0,0 +1 @@ +- Deprecated the generic age-18 variables (`is_child`, `is_adult`, `num_children`, `num_adults`, `family_type`, `child_index`, the eldest and youngest child and adult ages, `benunit_count_children`, `benunit_count_adults`, the disabled child and adult counts, `is_WA_adult`, `is_young_child`, `is_older_child`). They keep their formulas for downstream users, but no model rule reads them any more: each programme uses its own legal definition of a child or young person and of the claimant and partner. diff --git a/changelog.d/generic-child-adult-flags.fixed.md b/changelog.d/generic-child-adult-flags.fixed.md new file mode 100644 index 0000000000..0949c5f232 --- /dev/null +++ b/changelog.d/generic-child-adult-flags.fixed.md @@ -0,0 +1,4 @@ +- Fixed benefit-unit structure: a lone parent living with an 18- or 19-year-old still at school is no longer treated as a (married) couple, a couple with such a dependant is no longer treated as single or a lone parent, and a 16- or 17-year-old heading their own benefit unit is a single claimant rather than a couple with themselves as the child. Lone parent, single claimant and couple are now mutually exclusive. +- A benefit unit now has at most two claimants or partners: the head (the eldest adult head if more than one is given, or the eldest adult if the head is not an adult) and one partner (a member flagged as a parent if there is one, otherwise the eldest other adult). When the head is not flagged as a parent but two other members are, those two are the couple. Without relationship inputs, a member under 20 and at least 16 years younger than the head is presumed to be the head's child rather than their partner, so a lone parent with an 18-year-old at university stays single and a couple with one stays a couple of two. Datasets and users can supply `is_claimant_or_partner` directly. +- Fixed Marriage Allowance and the CPS marriage reforms to transfer allowance and split income only between spouses or civil partners, never from a child in the benefit unit; the Tax-Free Childcare work condition to test the applicant and partner only; the WTC lone parent, couple, disability and childcare elements and eligibility routes to use the tax credit claimant and child definitions; and the DfE schemes (Care to Learn, Childcare Grant, Parents' Learning Allowance, extended childcare income test) to use each scheme's own definition of a child or of the parent and partner. +- The HBAI equivalence scale now weights HBAI adults and dependent children, which changes factors only in households with no one aged 18 or over. diff --git a/changelog.d/legacy-child-definitions.fixed.md b/changelog.d/legacy-child-definitions.fixed.md new file mode 100644 index 0000000000..1a2c695a57 --- /dev/null +++ b/changelog.d/legacy-child-definitions.fixed.md @@ -0,0 +1,3 @@ +- Updated legacy benefits to use claimant and partner roles and programme-specific child definitions for legacy benefit allowances, income disregards, capital allocation, disability premiums, Housing Benefit childcare charges, and Council Tax Reduction exemptions. +- A child or young person placed with the family by a local authority is no longer counted as a member of the family for the legacy benefits (Income Support Regulations 1987 reg 16(4); Housing Benefit Regulations 2006 reg 21(3)). +- Housing Benefit's pension-age route now tests the claimant and any partner (SI 2014/1230 reg 6A(4)), so a pensioner living with an 18- or 19-year-old dependant can make a new Housing Benefit claim. diff --git a/changelog.d/lha-benefit-cap-claimant-definitions.fixed.md b/changelog.d/lha-benefit-cap-claimant-definitions.fixed.md new file mode 100644 index 0000000000..6366cbbe64 --- /dev/null +++ b/changelog.d/lha-benefit-cap-claimant-definitions.fixed.md @@ -0,0 +1,2 @@ +- Replaced age-18 child/adult flags in the LHA shared accommodation rate and the benefit-cap rates with claimant/partner status and responsibility for a child or young person. Because one LHA category and one benefit cap serve both Universal Credit and Housing Benefit, the family rates apply if either scheme's test is met (UC regs 4-5, including the reg 5(1)(a) route for 16-year-olds; HB reg 19), via the new `is_responsible_for_child_or_young_person_for_uc_or_housing_benefit`. +- Renters under 35 receiving attendance allowance, the DLA care component at the middle or highest rate, or the PIP daily living component are now excepted from the shared accommodation rate (UC Regulations 2013 Sch 4 para 29(5)). The exception is UC's; the Housing Benefit "young individual" definition (HB Regulations 2006 reg 2(1)) has no disability exception, but the model uses one LHA category for both schemes. The other paragraph 29 exceptions and the conservative LHA non-dependant proxy remain as documented limits. diff --git a/changelog.d/means-test-income-members.fixed.md b/changelog.d/means-test-income-members.fixed.md new file mode 100644 index 0000000000..9471d1b80c --- /dev/null +++ b/changelog.d/means-test-income-members.fixed.md @@ -0,0 +1 @@ +- Universal Credit, Housing Benefit, Income Support, tax credit and Council Tax Reduction means tests now count only the income of the claimant, any partner and the programme's own children or young persons, the members the model already counted. The income of any other member of the benefit unit no longer counts: for example an 18- or 19-year-old who is presumed to be the claimant's child but is neither a partner nor a qualifying young person. The same applies to the hours behind the Housing Benefit worker disregard. The shared helper is `add_for_members`. The regulations count only the claimant's and partner's income (UC Regulations 2013 reg 22; HB Regulations 2006 reg 25; IS Regulations 1987 reg 23; Tax Credits Act 2002 s.7; Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Sch 1 para 11). Dropping dependants' own income as well is left to a follow-up, because some survey records give children adult-level incomes. diff --git a/changelog.d/pension-credit-claimant-partner.fixed.md b/changelog.d/pension-credit-claimant-partner.fixed.md new file mode 100644 index 0000000000..6002399a75 --- /dev/null +++ b/changelog.d/pension-credit-claimant-partner.fixed.md @@ -0,0 +1,3 @@ +- Applied Pension Credit age conditions, income assessment and carer and severe disability additions to claimants and partners rather than dependent children and young people, and removed the severe disability addition's child veto. +- The Pension Credit severe disability addition now follows SPC Regulations 2002 Sch I para 1(1) and reg 6(5): a claimant with a partner qualifies if both receive a qualifying disability benefit and a carer benefit is paid for at most one of them (double amount when none is paid, single amount when one is), or if one receives it, the other is blind and no carer benefit is paid for the first (single amount). The person cared for is not observed, so each carer benefit paid in the benefit unit is taken to be for a different claimant or partner, and never for its recipient (a qualifying claimant's own Carer's Allowance does not bar them). The household non-dependant residence test and the mixed-age transitional protection remain unmodelled. +- Guarantee Credit is now paid only to benefit units eligible for Pension Credit, so it can no longer act as a passport (for example to targeted childcare) for families that do not qualify. diff --git a/changelog.d/uc-claimant-rules.fixed.md b/changelog.d/uc-claimant-rules.fixed.md new file mode 100644 index 0000000000..7156013368 --- /dev/null +++ b/changelog.d/uc-claimant-rules.fixed.md @@ -0,0 +1 @@ +- Apply Universal Credit age conditions, childcare work conditions, and household capital allocation to claimants and partners, with documented proxies for the observable minimum-age exceptions from age 16. Rename the capital allocation helper to `household_uc_unreported_claimants`. diff --git a/policyengine_uk/dynamics/demographics.py b/policyengine_uk/dynamics/demographics.py new file mode 100644 index 0000000000..8bbaca459f --- /dev/null +++ b/policyengine_uk/dynamics/demographics.py @@ -0,0 +1,43 @@ +"""Benefit-unit composition for the labour supply elasticity groups. + +The OBR labour supply note (Tables A1 and A2) assigns elasticities by +partnership and by the presence and age of the youngest child, without +defining a child. The dynamics modules keep an explicit age band for this: +benefit-unit members under 18 are children and members aged 18 or over are +adults. This is a modelling choice for the elasticity groups, not a legal +definition. + +Reference: https://obr.uk/docs/dlm_uploads/NICS-Cut-Impact-on-Labour-Supply-Note.pdf +""" + +import numpy as np +import pandas as pd + + +def benunit_age_18_composition(sim) -> pd.DataFrame: + """Per-person view of their benefit unit, split at age 18. + + Returns a DataFrame aligned with the person entity, with columns: + - count_under_18: benefit-unit members under 18; + - youngest_under_18_age: age of the youngest such member (inf if none); + - count_aged_18_or_over: benefit-unit members aged 18 or over. + """ + age = np.asarray(sim.calculate("age"), dtype=float) + # Position of each person's benefit unit, from the simulation's own entity + # structure (works for datasets and hand-built situations alike). + benunit_id = np.asarray(sim.populations["benunit"].members_entity_id) + under_18 = age < 18 + people = pd.DataFrame( + { + "benunit_id": benunit_id, + "under_18": under_18, + "aged_18_or_over": ~under_18, + "age_if_under_18": np.where(under_18, age, np.inf), + } + ) + by_benunit = people.groupby("benunit_id").agg( + count_under_18=("under_18", "sum"), + youngest_under_18_age=("age_if_under_18", "min"), + count_aged_18_or_over=("aged_18_or_over", "sum"), + ) + return by_benunit.reindex(benunit_id).reset_index(drop=True) diff --git a/policyengine_uk/dynamics/participation.py b/policyengine_uk/dynamics/participation.py index d7f4c1672e..f94af4fd79 100644 --- a/policyengine_uk/dynamics/participation.py +++ b/policyengine_uk/dynamics/participation.py @@ -12,6 +12,7 @@ from policyengine_uk import Simulation import warnings +from policyengine_uk.dynamics.demographics import benunit_age_18_composition from policyengine_uk.model_api import WEEKS_IN_YEAR # Weekly hours treated as full time when putting non-workers on a comparable @@ -38,13 +39,14 @@ def calculate_participation_elasticities( # Get demographic characteristics gender = sim.calculate("gender") is_married = sim.calculate("is_married", map_to="person") - has_children = sim.calculate("benunit_count_children", map_to="person") > 0 - youngest_child_age = sim.calculate("youngest_child_age", map_to="person") + composition = benunit_age_18_composition(sim) + has_children = composition["count_under_18"].values > 0 + youngest_child_age = composition["youngest_under_18_age"].values is_single = ~is_married # Get partner employment status for married individuals is_household_head = sim.calculate("is_household_head", map_to="person") - benunit_count_adults = sim.calculate("benunit_count_adults", map_to="person") + benunit_count_adults = composition["count_aged_18_or_over"].values employment_income = sim.calculate("employment_income") benunit_id = sim.calculate("benunit_id", map_to="person") adult_index = sim.calculate("adult_index") @@ -53,14 +55,16 @@ def calculate_participation_elasticities( df = pd.DataFrame( { "benunit_id": benunit_id, - "is_adult": adult_index > 0, + "is_aged_18_or_over": adult_index > 0, "employed": employment_income > 0, - "benunit_count_adults": benunit_count_adults, + "count_aged_18_or_over": benunit_count_adults, } ) # Calculate total employed adults per benunit - benunit_employed = df[df["is_adult"]].groupby("benunit_id")["employed"].sum() + benunit_employed = ( + df[df["is_aged_18_or_over"]].groupby("benunit_id")["employed"].sum() + ) # Map back to individuals employed_adults_in_benunit = df["benunit_id"].map(benunit_employed).fillna(0) diff --git a/policyengine_uk/dynamics/progression.py b/policyengine_uk/dynamics/progression.py index 72235e42ee..a15c345250 100644 --- a/policyengine_uk/dynamics/progression.py +++ b/policyengine_uk/dynamics/progression.py @@ -10,6 +10,7 @@ import numpy as np import pandas as pd from policyengine_uk import Simulation +from policyengine_uk.dynamics.demographics import benunit_age_18_composition from policyengine_uk.utils.marginal_rates import marginal_rate_step @@ -212,8 +213,9 @@ def calculate_labour_substitution_elasticities( # Get demographic characteristics for elasticity assignment gender = sim.calculate("gender") is_married = sim.calculate("is_married", map_to="person") - has_children = sim.calculate("benunit_count_children", map_to="person") > 0 - youngest_child_age = sim.calculate("youngest_child_age", map_to="person") + composition = benunit_age_18_composition(sim) + has_children = composition["count_under_18"].values > 0 + youngest_child_age = composition["youngest_under_18_age"].values # Initialize elasticity array elasticities = np.zeros(gender.shape, dtype=float) @@ -282,8 +284,9 @@ def calculate_labour_net_income_elasticities( # Get demographic characteristics for elasticity assignment gender = sim.calculate("gender") is_married = sim.calculate("is_married", map_to="person") - has_children = sim.calculate("benunit_count_children", map_to="person") > 0 - youngest_child_age = sim.calculate("youngest_child_age", map_to="person") + composition = benunit_age_18_composition(sim) + has_children = composition["count_under_18"].values > 0 + youngest_child_age = composition["youngest_under_18_age"].values # Initialize elasticity array elasticities = np.zeros(gender.shape, dtype=float) diff --git a/policyengine_uk/model_api.py b/policyengine_uk/model_api.py index 367801d235..597de7c32c 100644 --- a/policyengine_uk/model_api.py +++ b/policyengine_uk/model_api.py @@ -3,5 +3,6 @@ from policyengine_core import periods from microdf import MicroSeries, MicroDataFrame from policyengine_uk.utils.scenario import Scenario +from policyengine_uk.utils.benefit_unit import add_for_members GBP = "currency-GBP" diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml new file mode 100644 index 0000000000..26f65c2490 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -0,0 +1,12 @@ +description: Disability benefits whose recipients under 35 are excepted from the shared accommodation rate. +values: + 2013-04-29: + - attendance_allowance + - dla_sc_middle_plus + - pip_dl +metadata: + unit: list + label: shared accommodation rate disability exceptions + reference: + - title: Universal Credit Regulations 2013 Sch 4 para 29(5) + href: https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/29 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/disabled.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/disabled.yaml new file mode 100644 index 0000000000..bf1cfa9654 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/disabled.yaml @@ -0,0 +1,9 @@ +description: The exclusive annual age limit approximating the first Monday in September following the sixteenth birthday for Housing Benefit childcare charges for a disabled child. +values: + 2006-03-06: 17 +metadata: + unit: year + label: housing benefit childcare disabled child age limit + reference: + title: Housing Benefit Regulations 2006, regulation 28(6) and (13) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/28 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/standard.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/standard.yaml new file mode 100644 index 0000000000..ad5e7aef94 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/childcare/child_age_limit/standard.yaml @@ -0,0 +1,9 @@ +description: The exclusive annual age limit approximating the first Monday in September following the fifteenth birthday for Housing Benefit childcare charges. +values: + 2006-03-06: 16 +metadata: + unit: year + label: housing benefit childcare standard child age limit + reference: + title: Housing Benefit Regulations 2006, regulation 28(6) and (13) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/28 diff --git a/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/older.yaml b/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/older.yaml new file mode 100644 index 0000000000..3c274508fa --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/older.yaml @@ -0,0 +1,9 @@ +description: Income Support personal allowance age threshold for older single claimants. +values: + 1988-04-11: 25 +metadata: + unit: year + label: income support older allowance age threshold + reference: + - title: Income Support (General) Regulations 1987, Schedule 2 paragraph 1 + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/2 diff --git a/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/younger.yaml b/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/younger.yaml new file mode 100644 index 0000000000..73bbf7a1e3 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/income_support/amounts/age_threshold/younger.yaml @@ -0,0 +1,9 @@ +description: Income Support personal allowance age threshold for young claimants and partners. +values: + 1988-04-11: 18 +metadata: + unit: year + label: income support younger allowance age threshold + reference: + - title: Income Support (General) Regulations 1987, Schedule 2 paragraph 1 + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/2 diff --git a/policyengine_uk/parameters/gov/dwp/income_support/eligibility/lone_parent_youngest_child_age_limit.yaml b/policyengine_uk/parameters/gov/dwp/income_support/eligibility/lone_parent_youngest_child_age_limit.yaml new file mode 100644 index 0000000000..9ea08c5cba --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/income_support/eligibility/lone_parent_youngest_child_age_limit.yaml @@ -0,0 +1,9 @@ +description: Income Support lone-parent youngest-child age limit, applied inclusively under the existing annual-age approximation although legislation specifies under this age. +values: + 2012-05-21: 5 +metadata: + unit: year + label: income support lone parent child age limit + reference: + - title: Income Support (General) Regulations 1987, Schedule 1B paragraph 1 + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/1B diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/disabled.yaml b/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/disabled.yaml new file mode 100644 index 0000000000..561f2af9af --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/disabled.yaml @@ -0,0 +1,9 @@ +description: Annual age below which a disabled child's childcare charges count for the Working Tax Credit childcare element, approximating the week containing the 1 September after the child's 16th birthday. +values: + 2003-04-06: 17 +metadata: + unit: year + label: WTC childcare disabled child age limit + reference: + - title: Working Tax Credit (Entitlement and Maximum Rate) Regulations 2002 reg 14(3)-(4) + href: https://www.legislation.gov.uk/uksi/2002/2005/regulation/14 diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/standard.yaml b/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/standard.yaml new file mode 100644 index 0000000000..baf97f17e2 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/tax_credits/working_tax_credit/childcare_child_age_limit/standard.yaml @@ -0,0 +1,9 @@ +description: Annual age below which a child's childcare charges count for the Working Tax Credit childcare element, approximating the week containing the 1 September after the child's 15th birthday. +values: + 2003-04-06: 16 +metadata: + unit: year + label: WTC childcare child age limit + reference: + - title: Working Tax Credit (Entitlement and Maximum Rate) Regulations 2002 reg 14(3) + href: https://www.legislation.gov.uk/uksi/2002/2005/regulation/14 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/min_age.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/min_age.yaml new file mode 100644 index 0000000000..a97bf9116d --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/min_age.yaml @@ -0,0 +1,9 @@ +description: Default minimum age for Universal Credit claimants. +values: + 2013-04-29: 18 +metadata: + unit: year + label: universal credit minimum age + reference: + - title: Welfare Reform Act 2012, section 4(1)(a) + href: https://www.legislation.gov.uk/ukpga/2012/5/section/4 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/reduced_min_age.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/reduced_min_age.yaml new file mode 100644 index 0000000000..5a4c2c77b5 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/eligibility/reduced_min_age.yaml @@ -0,0 +1,9 @@ +description: Reduced minimum age for Universal Credit claimants meeting an age exception. +values: + 2013-04-29: 16 +metadata: + unit: year + label: universal credit reduced minimum age + reference: + - title: The Universal Credit Regulations 2013, regulation 8(1) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/8 diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml new file mode 100644 index 0000000000..83ffb9fedd --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml @@ -0,0 +1,6 @@ +description: Age below which a benefit-unit member much younger than the head is presumed to be the head's child rather than their partner, when relationships are not supplied. This is a PolicyEngine modelling presumption, not a statutory rule. +values: + 0001-01-01: 20 +metadata: + unit: year + label: presumed child age limit diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml new file mode 100644 index 0000000000..19f9dbe80f --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml @@ -0,0 +1,6 @@ +description: Minimum age gap below the benefit-unit head at which a younger member is presumed to be the head's child rather than their partner, when relationships are not supplied. This is a PolicyEngine modelling presumption, not a statutory rule. +values: + 0001-01-01: 16 +metadata: + unit: year + label: presumed child minimum age gap diff --git a/policyengine_uk/parameters/household/demographic/equiv/child_age_threshold.yaml b/policyengine_uk/parameters/household/demographic/equiv/child_age_threshold.yaml new file mode 100644 index 0000000000..975a248143 --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/equiv/child_age_threshold.yaml @@ -0,0 +1,9 @@ +description: Age from which a dependent child takes the older-child weight in the Households Below Average Income modified OECD equivalence scale. +values: + 2010-04-01: 14 +metadata: + unit: year + label: HBAI equivalence scale older-child age + reference: + - title: HBAI background information and methodology FYE 2025, Table 4 (equivalence scales) + href: https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#equivalisation-1 diff --git a/policyengine_uk/parameters/household/demographic/hbai/dependent_child/age_limit.yaml b/policyengine_uk/parameters/household/demographic/hbai/dependent_child/age_limit.yaml new file mode 100644 index 0000000000..cd1938b97e --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/hbai/dependent_child/age_limit.yaml @@ -0,0 +1,9 @@ +description: Age below which every person is a dependent child in Households Below Average Income statistics. +values: + 0001-01-01: 16 +metadata: + unit: year + label: HBAI dependent child age limit + reference: + - title: HBAI background information and methodology FYE 2025, glossary (Child) + href: https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#child diff --git a/policyengine_uk/parameters/household/demographic/hbai/dependent_child/young_person_age_limit.yaml b/policyengine_uk/parameters/household/demographic/hbai/dependent_child/young_person_age_limit.yaml new file mode 100644 index 0000000000..1cc7f68e81 --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/hbai/dependent_child/young_person_age_limit.yaml @@ -0,0 +1,9 @@ +description: Age below which a person in full-time non-advanced education or unwaged government training, living with parents and without a partner, is a dependent child in Households Below Average Income statistics. +values: + 0001-01-01: 20 +metadata: + unit: year + label: HBAI dependent young person age limit + reference: + - title: HBAI background information and methodology FYE 2025, glossary (Child) + href: https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#child diff --git a/policyengine_uk/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index c10ff49f3b..813cfc4570 100644 --- a/policyengine_uk/reforms/cps/marriage_tax_reforms.py +++ b/policyengine_uk/reforms/cps/marriage_tax_reforms.py @@ -13,13 +13,17 @@ class meets_expanded_ma_conditions(Variable): value_type = bool def formula(person, period): - # There is a child who either meets the age condition or the education condition + # The couple has a child (a benefit-unit member other than the + # couple) under the age limit, or meeting the education condition. benunit = person.benunit + is_couple_child = ~person("is_claimant_or_partner", period) if max_child_age is not None: - child_meets_age_condition = person("age", period) <= max_child_age + child_meets_age_condition = is_couple_child & ( + person("age", period) < max_child_age + ) return benunit.any(child_meets_age_condition) if child_education_levels is not None: - child_meets_education_condition = np.isin( + child_meets_education_condition = is_couple_child & np.isin( person("education_level", period).decode_to_str(), child_education_levels, ) @@ -58,8 +62,10 @@ class marriage_allowance(Variable): def formula(person, period, parameters): marital = person("marital_status", period) married = marital == marital.possible_values.MARRIED - eligible = married & person( - "meets_marriage_allowance_income_conditions", period + eligible = ( + married + & person("is_claimant_or_partner", period) + & person("meets_marriage_allowance_income_conditions", period) ) transferable_amount = person("partners_unused_personal_allowance", period) allowances = parameters(period).gov.hmrc.income_tax.allowances @@ -73,7 +79,9 @@ def formula(person, period, parameters): capped_percentage, ) max_amount = allowances.personal_allowance.amount * capped_percentage - amount_if_eligible_pre_rounding = min_(transferable_amount, max_amount) + amount_if_eligible_pre_rounding = max_( + min_(transferable_amount, max_amount), 0 + ) # Round up. rounding_increment = allowances.marriage_allowance.rounding_increment amount_if_eligible = ( @@ -103,13 +111,17 @@ class meets_ma_neutral_tax_conditions(Variable): value_type = bool def formula(person, period): - # There is a child who either meets the age condition or the education condition + # The couple has a child (a benefit-unit member other than the + # couple) under the age limit, or meeting the education condition. benunit = person.benunit + is_couple_child = ~person("is_claimant_or_partner", period) if max_child_age is not None: - child_meets_age_condition = person("age", period) <= max_child_age + child_meets_age_condition = is_couple_child & ( + person("age", period) < max_child_age + ) return benunit.any(child_meets_age_condition) if child_education_levels is not None: - child_meets_education_condition = np.isin( + child_meets_education_condition = is_couple_child & np.isin( person("education_level", period).decode_to_str(), child_education_levels, ) @@ -150,9 +162,11 @@ class adjusted_net_income(Variable): def formula(person, period, parameters): income = person("unadjusted_net_income", period) - is_adult = person("is_adult", period) - total_income = person.benunit.sum(is_adult * income) - has_spouse = person.benunit("is_married", period) & is_adult + # Split between the spouses or civil partners only, never their + # children. + couple_member = person("is_claimant_or_partner", period) + total_income = person.benunit.sum(couple_member * income) + has_spouse = person.benunit("is_married", period) & couple_member originally_split_income_branch = person.simulation.get_branch( "originally_split_income", clone_system=True diff --git a/policyengine_uk/tests/code_health/test_no_generic_child_adult_flags.py b/policyengine_uk/tests/code_health/test_no_generic_child_adult_flags.py new file mode 100644 index 0000000000..083e88e825 --- /dev/null +++ b/policyengine_uk/tests/code_health/test_no_generic_child_adult_flags.py @@ -0,0 +1,96 @@ +"""No model code may read the deprecated generic child and adult variables. + +UK law has no single definition of a child or an adult: each programme sets +its own (Child Benefit, Universal Credit, tax credits, Pension Credit, the +legacy means-tested benefits, student support, ...), and statistics follow +the Households Below Average Income definitions. The generic variables below +use age-18 cut-offs with no legal basis. They are kept, with their original +formulas, only because downstream packages still read them. +""" + +import re +from pathlib import Path + +import pytest + +from policyengine_uk.system import system + +PACKAGE_ROOT = Path(__file__).resolve().parents[2] + +DEPRECATED = { + "is_child", + "is_adult", + "num_children", + "num_adults", + "benunit_count_children", + "benunit_count_adults", + "eldest_child_age", + "youngest_child_age", + "eldest_adult_age", + "youngest_adult_age", + "child_index", + "is_eldest_child", + "is_benunit_eldest_child", + "num_disabled_children", + "num_enhanced_disabled_children", + "num_severely_disabled_children", + "num_disabled_adults", + "num_enhanced_disabled_adults", + "num_severely_disabled_adults", + "family_type", + "is_WA_adult", + "is_young_child", + "is_older_child", +} + +# A shim may read another shim, so that each keeps its original formula. +SHIM_FILES = { + PACKAGE_ROOT / "variables" / "household" / "demographic" / f"{name}.py" + for name in DEPRECATED +} | { + PACKAGE_ROOT / "variables" / "household" / "demographic" / "benunit" / f"{name}.py" + for name in DEPRECATED +} + +REFERENCE = re.compile( + r"""["'](""" + "|".join(sorted(DEPRECATED, key=len, reverse=True)) + r""")["']""" +) + + +def _model_files(): + for path in sorted(PACKAGE_ROOT.rglob("*")): + if path.suffix not in (".py", ".yaml"): + continue + if "tests" in path.relative_to(PACKAGE_ROOT).parts: + continue + if path in SHIM_FILES: + continue + yield path + + +def test_shims_still_exist_for_downstream_users(): + missing = sorted(name for name in DEPRECATED if name not in system.variables) + assert not missing, f"Deprecated variables removed too early: {missing}" + + +def test_shims_are_labelled_deprecated(): + unlabelled = sorted( + name + for name in DEPRECATED + if "deprecated" not in (system.variables[name].label or "").lower() + ) + assert not unlabelled, ( + f"Deprecated variables without a deprecated label: {unlabelled}" + ) + + +@pytest.mark.parametrize( + "path", list(_model_files()), ids=lambda p: str(p.relative_to(PACKAGE_ROOT)) +) +def test_model_code_does_not_read_generic_child_or_adult_flags(path): + hits = sorted({m.group(1) for m in REFERENCE.finditer(path.read_text())}) + assert not hits, ( + f"{path.relative_to(PACKAGE_ROOT)} reads deprecated generic variables " + f"{hits}. Use the programme's own legal definition, an explicit age " + "predicate, or an HBAI variable (hbai in the name) instead." + ) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml new file mode 100644 index 0000000000..b1435b45d5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -0,0 +1,261 @@ +# UC Regulations 2013, Schedule 4 paragraphs 9 and 27-28: +# https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 +# The existing bedroom calculation is unchanged: B = 1 bedroom, C = 2; +# A is the shared accommodation rate, overriding the bedroom calculation. +- name: Single 17-year-old renter is not their own dependent child + period: 2025 + input: + age: 17 + output: + is_claimant_or_partner: true + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + lha_renter_has_non_dependant: false + is_lha_shared_accommodation_rate_specified_renter: true + # Single; 17 < 35; no dependant or non-dependant: A (previously B). + LHA_category: A + +- name: Single 30-year-old renter remains subject to the shared rate + period: 2025 + input: + age: 30 + output: + lha_renter_has_non_dependant: false + is_lha_shared_accommodation_rate_specified_renter: true + # Single; 30 < 35; no other occupants: A. + LHA_category: A + +- name: Single renter immediately below the shared rate age threshold + period: 2025 + input: + age: 34 + output: + is_lha_shared_accommodation_rate_specified_renter: true + # 34 < 35: A. + LHA_category: A + +- name: Single renter at the shared rate age threshold + period: 2025 + input: + age: 35 + output: + is_lha_shared_accommodation_rate_specified_renter: false + # 35 is not below 35; 1 bedroom: B. + LHA_category: B + +- name: A 17-year-old apprentice in the renter's benefit unit is a non-dependant + period: 2025 + input: + people: + renter: + age: 30 + is_parent: true + apprentice: + age: 17 + current_education: NOT_IN_EDUCATION + is_apprentice: true + benunits: + family: + members: [renter, apprentice] + households: + home: + members: [renter, apprentice] + output: + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + lha_renter_has_non_dependant: true + is_lha_shared_accommodation_rate_specified_renter: false + # No UC QYP, but a non-dependant blocks A. Existing bedroom formula = 1: B. + LHA_category: B + +- name: Lone parent with an 18-year-old qualifying young person + period: 2025 + input: + people: + renter: + age: 30 + is_parent: true + student: + age: 18 + current_education: POST_SECONDARY + benunits: + family: + members: [renter, student] + households: + home: + members: [renter, student] + output: + is_couple: false + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true + lha_renter_has_non_dependant: false + is_lha_shared_accommodation_rate_specified_renter: false + # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. + LHA_category: B + +- name: Couple aged 30 and 17 is not a single specified renter + period: 2025 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + benunits: + couple: + members: [renter, partner] + households: + home: + members: [renter, partner] + output: + is_couple: true + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + lha_renter_has_non_dependant: false + is_lha_shared_accommodation_rate_specified_renter: false + # A partner establishes neither QYP responsibility nor a non-dependant; + # couple room = 1: B under the existing bedroom calculation. + LHA_category: B + +- name: A 17-year-old claimant in another benefit unit blocks the shared rate + period: 2025 + input: + people: + renter: + age: 30 + apprentice: + age: 17 + current_education: NOT_IN_EDUCATION + is_apprentice: true + benunits: + renter_unit: + members: [renter] + apprentice_unit: + members: [apprentice] + households: + home: + members: [renter, apprentice] + output: + lha_renter_has_non_dependant: [true, true] + is_lha_shared_accommodation_rate_specified_renter: [false, false] + # Other unit's claimant blocks A; 1 + 1 occupant aged 16+ outside = 2: C. + # Renter previously A because the other claimant was under 18. + LHA_category: [C, C] + +- name: A 17-year-old student claimant in another unit also blocks the shared rate + period: 2025 + input: + people: + renter: + age: 30 + student_claimant: + age: 17 + current_education: POST_SECONDARY + benunits: + renter_unit: + members: [renter] + student_unit: + members: [student_claimant] + households: + home: + members: [renter, student_claimant] + output: + lha_renter_has_non_dependant: [true, true] + # Other claimant blocks A even if their age/education meets the QYP test. + # 1 + 1 occupant aged 16+ outside = 2: C (renter previously A). + LHA_category: [C, C] + +- name: Rent liability of another benefit unit does not exempt its claimant + period: 2025 + input: + people: + renter: + age: 30 + other_renter: + age: 40 + benunits: + renter_unit: + members: [renter] + benunit_rent: 12000 + other_unit: + members: [other_renter] + benunit_rent: 12000 + households: + home: + members: [renter, other_renter] + output: + eldest_claimant_or_partner_age: [30, 40] + lha_renter_has_non_dependant: [true, true] + # Conservative proxy retained: 1 + 1 occupant aged 16+ outside = 2: C. + LHA_category: [C, C] + +- name: Occupants in a different household do not block the shared rate + period: 2025 + input: + people: + renter: + age: 30 + other_renter: + age: 40 + benunits: + renter_unit: + members: [renter] + other_unit: + members: [other_renter] + households: + first_home: + members: [renter] + second_home: + members: [other_renter] + output: + lha_renter_has_non_dependant: [false, false] + # Single, 30 < 35, nobody else in this household: A. + # Other renter 40 >= 35; 1 bedroom: B. + LHA_category: [A, B] + +- name: Actual shared accommodation retains its separate route to category A + period: 2025 + input: + age: 40 + is_shared_accommodation: true + output: + is_lha_shared_accommodation_rate_specified_renter: false + # 40 >= 35, but actual shared accommodation independently gives A. + LHA_category: A + +- name: A disabled 17-year-old renter is excepted from the shared accommodation rate + # UC Sch 4 para 29(5)(c): a renter under 35 receiving the PIP daily living + # component is an excepted person, so the one-bedroom rate applies: B. + period: 2025 + input: + age: 17 + pip_dl: 3_000 + output: + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B + +- name: A disabled 16-year-old renter is excepted from the shared accommodation rate + # UC Sch 4 para 29(5)(a): the DLA care component at the middle or highest + # rate makes a renter under 35 an excepted person: B. + period: 2025 + input: + age: 16 + dla_sc_middle_plus: true + output: + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B + +- name: A 16-year-old renter without a disability benefit gets the shared rate + period: 2025 + input: + age: 16 + output: + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A + +- name: A renter under 35 on attendance allowance is excepted from the shared rate + period: 2025 + input: + age: 30 + attendance_allowance: 3_000 + output: + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml index bdbbef68e0..c27645ce44 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml @@ -99,7 +99,7 @@ esa_income_eligible: false esa_income: 0 -- name: Income-related ESA allocates household capital by adult share when nobody reports the award +- name: Income-related ESA allocates household capital by claimant and partner share when nobody reports the award period: 2026 absolute_error_margin: 0 input: @@ -178,7 +178,6 @@ income_support_reported: 1 child: age: 4 - is_child: true benunits: benunit: members: [claimant, child] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml new file mode 100644 index 0000000000..beed6513c8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml @@ -0,0 +1,77 @@ +# https://www.legislation.gov.uk/uksi/2008/794/regulation/83 +# Only claimant and partner contribute capital weights. The equal allocation of +# household stocks is a PolicyEngine convention, not a statutory allocation rule. +- name: Income-related ESA capital allocation includes a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # Two claimants: each gets 18,000 * 1 / 2 = 9,000. + # Previously the under-18 claimant had zero weight: [0, 18,000]. + esa_income_assessable_capital: [9_000, 9_000] + +- name: Income-related ESA capital allocation excludes a dependent student aged 19 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_parent: true + student: + age: 19 + is_in_non_advanced_education: true + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant, student] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, student, unrelated_claimant] + savings: 18_000 + output: + # The dependent student adds no weight. Each claimant gets 18,000 / 2. + # Previously the student's unit got 18,000 * 2 / 3 = 12,000 (other: 6,000). + esa_income_assessable_capital: [9_000, 9_000] + +- name: Income-related ESA claim path recognises a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + esa_income_reported: 1_000 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # The model's claim-path convention assigns all 18,000 to the claimant + # and zero to the non-claiming unit. Previously the age-18 gate yielded + # fallback allocation [0, 18,000] despite this claimant's claim path. + esa_income_assessable_capital: [18_000, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml index c6aa7c77a9..411dd36183 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml @@ -1,27 +1,242 @@ - name: One child, capped period: 2025 - absolute_error_margin: 100 + absolute_error_margin: 0.01 input: - benunit_count_children: 1 - childcare_expenses: 9_000 + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 10 + benunits: + benunit: + members: [claimant, child0] output: - housing_benefit_applicable_income_childcare_element: 9_000 + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 1 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 1 + housing_benefit_applicable_income_childcare_element: 9000 - name: Two child, capped period: 2025 - absolute_error_margin: 100 + absolute_error_margin: 0.01 input: - benunit_count_children: 2 - childcare_expenses: 10_000 + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 10000 + child0: + age: 8 + child1: + age: 10 + benunits: + benunit: + members: [claimant, child0, child1] output: - housing_benefit_applicable_income_childcare_element: 10_000 + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 2 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 2 + housing_benefit_applicable_income_childcare_element: 10000 - name: No children period: 2025 - absolute_error_margin: 100 + absolute_error_margin: 0.01 input: - benunit_count_children: 0 - childcare_expenses: 30_000 + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 30000 + benunits: + benunit: + members: [claimant] output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 0 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 0 housing_benefit_applicable_income_childcare_element: 0 +- name: Non-disabled child aged 15 remains within annual age approximation + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 15 + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 1 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 1 + housing_benefit_applicable_income_childcare_element: 9000 + +- name: Non-disabled young person aged 16 no longer counts + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 16 + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 0 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 0 + housing_benefit_applicable_income_childcare_element: 0 + +- name: Non-disabled young person aged 17 no longer counts + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 17 + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 0 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 0 + housing_benefit_applicable_income_childcare_element: 0 + +- name: Disabled young person aged 16 counts + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 16 + is_disabled_for_benefits: true + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 1 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 1 + housing_benefit_applicable_income_childcare_element: 9000 + +- name: Blind young person aged 16 counts + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 16 + is_blind: true + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 1 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 1 + housing_benefit_applicable_income_childcare_element: 9000 + +- name: Disabled young person aged 17 no longer counts + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9000 + child0: + age: 17 + is_disabled_for_benefits: true + benunits: + benunit: + members: [claimant, child0] + output: + # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). + # With 0 relevant children the deduction is min(cost, weekly cap * 52). + num_relevant_children_for_housing_benefit_childcare: 0 + housing_benefit_applicable_income_childcare_element: 0 + +- name: One relevant child and an older young person use the one-child cap + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 30_000 + child: + age: 10 + young_person: + age: 17 + benunits: + benunit: + members: [claimant, child, young_person] + output: + num_relevant_children_for_housing_benefit_childcare: 1 + # Reg 27(3) selects the one-child cap: min(30,000, 312.0555942 * 52). + # The existing uprated WTC cap is retained; its difference from the + # statutory HB cap of 175/week is outside this classification change. + housing_benefit_applicable_income_childcare_element: 16_226.8908984 + +- name: Under-18 claimant is not their own relevant child + period: 2025 + input: + people: + claimant: + age: 17 + childcare_expenses: 9_000 + benunits: + benunit: + members: [claimant] + output: + # 0 dependants implies 0 childcare cap (HB reg 27(3)). + num_relevant_children_for_housing_benefit_childcare: 0 + housing_benefit_applicable_income_childcare_element: 0 + +- name: Disabled 16-year-old outside education and training is not a relevant young person + period: 2025 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 9_000 + young_person: + age: 16 + is_disabled_for_benefits: true + is_in_non_advanced_education: false + is_in_approved_training: false + benunits: + benunit: + members: [claimant, young_person] + output: + # HB reg 28(6) requires a child of the family, including a qualifying + # young person; the disability age extension does not create that status. + num_relevant_children_for_housing_benefit_childcare: 0 + housing_benefit_applicable_income_childcare_element: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_applicable_amount.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_applicable_amount.yaml index b1b03bc4bd..eeea8a6e58 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_applicable_amount.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_applicable_amount.yaml @@ -2,23 +2,15 @@ period: 2025 absolute_error_margin: 100 input: - people: - person1: - is_SP_age: false - person2: - is_SP_age: false - person3: - is_SP_age: false - person4: + people: + claimant: + age: 25 is_SP_age: false + is_claimant_or_partner: true benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant] housing_benefit_eligible: true - eldest_adult_age: 25 - is_single_person: true - is_couple: false - is_lone_parent: false benefits_premiums: 0 output: housing_benefit_applicable_amount: 90.5 * 52 @@ -27,23 +19,15 @@ period: 2025 absolute_error_margin: 100 input: - people: - person1: - is_SP_age: false - person2: - is_SP_age: false - person3: - is_SP_age: false - person4: + people: + claimant: + age: 23 is_SP_age: false + is_claimant_or_partner: true benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant] housing_benefit_eligible: true - eldest_adult_age: 23 - is_single_person: true - is_couple: false - is_lone_parent: false benefits_premiums: 0 output: housing_benefit_applicable_amount: 71.7 * 52 @@ -52,23 +36,19 @@ period: 2025 absolute_error_margin: 100 input: - people: - person1: - is_SP_age: false - person2: + people: + claimant: + age: 17 is_SP_age: false - person3: - is_SP_age: false - person4: + is_claimant_or_partner: true + is_parent: true + child: + age: 1 is_SP_age: false benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant, child] housing_benefit_eligible: true - eldest_adult_age: 17 - is_single_person: false - is_couple: false - is_lone_parent: true benefits_premiums: 0 output: housing_benefit_applicable_amount: 71.7 * 52 @@ -77,23 +57,19 @@ period: 2025 absolute_error_margin: 100 input: - people: - person1: - is_SP_age: false - person2: + people: + claimant: + age: 18 is_SP_age: false - person3: - is_SP_age: false - person4: + is_claimant_or_partner: true + is_parent: true + child: + age: 1 is_SP_age: false benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant, child] housing_benefit_eligible: true - eldest_adult_age: 18 - is_single_person: false - is_couple: false - is_lone_parent: true benefits_premiums: 0 output: housing_benefit_applicable_amount: 90.5 * 52 @@ -102,23 +78,19 @@ period: 2025 absolute_error_margin: 200 input: - people: - person1: - is_SP_age: false - person2: + people: + claimant: + age: 18 is_SP_age: false - person3: - is_SP_age: false - person4: + is_claimant_or_partner: true + partner: + age: 18 is_SP_age: false + is_claimant_or_partner: true benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant, partner] housing_benefit_eligible: true - eldest_adult_age: 18 - is_single_person: false - is_couple: true - is_lone_parent: false benefits_premiums: 0 output: housing_benefit_applicable_amount: 142.25 * 52 @@ -127,23 +99,19 @@ period: 2025 absolute_error_margin: 100 input: - people: - person1: - is_SP_age: false - person2: - is_SP_age: false - person3: + people: + claimant: + age: 17 is_SP_age: false - person4: + is_claimant_or_partner: true + partner: + age: 17 is_SP_age: false + is_claimant_or_partner: true benunits: benunit: - members: [person1, person2, person3, person4] + members: [claimant, partner] housing_benefit_eligible: true - eldest_adult_age: 17 - is_single_person: false - is_couple: true - is_lone_parent: false benefits_premiums: 0 output: housing_benefit_applicable_amount: 108.3 * 52 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml index e9a0cd47c3..f598331f3a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml @@ -129,7 +129,7 @@ housing_benefit_assessable_capital: 50_000 housing_benefit_eligible: false -- name: Mixed benunits split household Housing Benefit capital by adult share +- name: Mixed benunits split household Housing Benefit capital by claimant and partner share period: 2025 absolute_error_margin: 0 input: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml new file mode 100644 index 0000000000..b1ff157058 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml @@ -0,0 +1,52 @@ +# https://www.legislation.gov.uk/uksi/2006/213/regulation/25 +# Only claimant and partner contribute capital weights. The equal allocation of +# household stocks is a PolicyEngine convention, not a statutory allocation rule. +- name: Housing Benefit capital allocation includes a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # Two claimants: each gets 18,000 * 1 / 2 = 9,000. + # Previously the under-18 claimant had zero weight: [0, 18,000]. + housing_benefit_assessable_capital: [9_000, 9_000] + +- name: Housing Benefit capital allocation excludes a dependent student aged 19 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_parent: true + student: + age: 19 + is_in_non_advanced_education: true + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant, student] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, student, unrelated_claimant] + savings: 18_000 + output: + # The dependent student adds no weight. Each claimant gets 18,000 / 2. + # Previously the student's unit got 18,000 * 2 / 3 = 12,000 (other: 6,000). + housing_benefit_assessable_capital: [9_000, 9_000] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml new file mode 100644 index 0000000000..278930ea01 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml @@ -0,0 +1,205 @@ +- name: Lone parent with qualifying young person uses lone-parent rates + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 40 + weekly_hours: 0 + is_claimant_or_partner: true + is_parent: true + person1: + age: 18 + weekly_hours: 0 + is_in_non_advanced_education: true + benunits: + benunit: + members: [person0, person1] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 92.04; *52. + housing_benefit_applicable_amount: 4786.08 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 4786.08 + # HB Sch 4 paras 4/7/10: 25 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 1300 + +- name: Couple with qualifying young person uses one couple allowance + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 40 + weekly_hours: 0 + is_claimant_or_partner: true + is_parent: true + person1: + age: 38 + weekly_hours: 0 + is_claimant_or_partner: true + person2: + age: 18 + weekly_hours: 0 + is_in_non_advanced_education: true + benunits: + benunit: + members: [person0, person1, person2] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 144.67; *52. + housing_benefit_applicable_amount: 7522.84 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 7522.84 + # HB Sch 4 paras 4/7/10: 10 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 520 + +- name: Under-18 single claimant uses single rates + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 17 + weekly_hours: 0 + is_claimant_or_partner: true + benunits: + benunit: + members: [person0] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 72.92; *52. + housing_benefit_applicable_amount: 3791.84 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 3791.84 + # HB Sch 4 paras 4/7/10: 5 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 260 + +- name: Couple with under-18 partner uses couple rates + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 30 + weekly_hours: 0 + is_claimant_or_partner: true + person1: + age: 17 + weekly_hours: 0 + is_claimant_or_partner: true + benunits: + benunit: + members: [person0, person1] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 144.67; *52. + housing_benefit_applicable_amount: 7522.84 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 7522.84 + # HB Sch 4 paras 4/7/10: 10 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 520 + +- name: Lone under-18 parent uses younger lone-parent allowance + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 17 + weekly_hours: 0 + is_claimant_or_partner: true + is_parent: true + person1: + age: 1 + weekly_hours: 0 + benunits: + benunit: + members: [person0, person1] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 72.92; *52. + housing_benefit_applicable_amount: 3791.84 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 3791.84 + # HB Sch 4 paras 4/7/10: 25 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 1300 + +- name: Two under-18 claimants use younger couple allowance + period: 2025 + absolute_error_margin: 0.01 + input: + # Isolate the statutory Sch 4 categories from existing automatic uprating. + gov.dwp.housing_benefit.means_test.income_disregard.single: 5 + gov.dwp.housing_benefit.means_test.income_disregard.couple: 10 + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 25 + people: + person0: + age: 17 + weekly_hours: 0 + is_claimant_or_partner: true + person1: + age: 16 + weekly_hours: 0 + is_claimant_or_partner: true + benunits: + benunit: + members: [person0, person1] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1: the applicable weekly model rate is 110.14; *52. + housing_benefit_applicable_amount: 5727.28 + # CTR retains the existing shared HB allowance parameters. + council_tax_reduction_applicable_amount: 5727.28 + # HB Sch 4 paras 4/7/10: 10 * 52, with no hours disregard. + housing_benefit_applicable_income_disregard: 520 + +- name: Under-18 claimant allowance ignores an older qualifying dependant + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 17 + is_claimant_or_partner: true + qualifying_dependant: + age: 18 + is_claimant_or_partner: false + is_in_non_advanced_education: true + benunits: + benunit: + members: [claimant, qualifying_dependant] + housing_benefit_eligible: true + benefits_premiums: 0 + output: + # HB Sch 3 para 1(2): the claimant is under 18, even if responsible + # for an older qualifying young person. Younger rate 72.92 * 52. + housing_benefit_applicable_amount: 3_791.84 + # The shared CTR allowance calculation also uses only claimant/partner ages. + council_tax_reduction_applicable_amount: 3_791.84 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_capital.yaml index 585884ea9c..932f83c9d1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_capital.yaml @@ -8,7 +8,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -35,7 +34,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -57,7 +55,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -79,7 +76,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -103,7 +99,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -125,7 +120,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -138,7 +132,7 @@ income_support_eligible: false income_support: 0 -- name: Income Support allocates household capital by adult share across benunits +- name: Income Support allocates household capital by claimant and partner share across benunits period: 2021 absolute_error_margin: 1 input: @@ -221,7 +215,6 @@ income_support_reported: true c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_capital.yaml new file mode 100644 index 0000000000..e3f70982ef --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_capital.yaml @@ -0,0 +1,82 @@ +# https://www.legislation.gov.uk/uksi/1987/1967/regulation/23 +# Only claimant and partner contribute capital weights. The equal allocation of +# household stocks is a PolicyEngine convention, not a statutory allocation rule. +- name: Income Support capital allocation includes a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + would_claim_IS: false + unrelated_benunit: + members: [unrelated_claimant] + would_claim_IS: false + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # Two claimants: each gets 18,000 * 1 / 2 = 9,000. + # Previously the under-18 claimant had zero weight: [0, 18,000]. + income_support_assessable_capital: [9_000, 9_000] + +- name: Income Support capital allocation excludes a dependent student aged 19 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_parent: true + student: + age: 19 + is_in_non_advanced_education: true + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant, student] + would_claim_IS: false + unrelated_benunit: + members: [unrelated_claimant] + would_claim_IS: false + households: + household: + members: [claimant, student, unrelated_claimant] + savings: 18_000 + output: + # The dependent student adds no weight. Each claimant gets 18,000 / 2. + # Previously the student's unit got 18,000 * 2 / 3 = 12,000 (other: 6,000). + income_support_assessable_capital: [9_000, 9_000] + +- name: Income Support claim path recognises a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + would_claim_IS: true + unrelated_benunit: + members: [unrelated_claimant] + would_claim_IS: false + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # The model's claim-path convention assigns all 18,000 to the claimant + # and zero to the non-claiming unit. Previously the age-18 gate yielded + # fallback allocation [0, 18,000] despite this claimant's claim path. + income_support_assessable_capital: [18_000, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_family_definitions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_family_definitions.yaml new file mode 100644 index 0000000000..00ec78032a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_family_definitions.yaml @@ -0,0 +1,286 @@ +# IS Regulations 1987 Sch 2 para 1; Sch 8 paras 5, 6 and 10. +# Annual amounts use existing CPI-uprated 2025 weekly parameters * 52: +# low = 80.01221579759817 * 52 = 4160.635221475105; +# high = 101.00013997540619 * 52 = 5252.007278721122; +# couple = 158.66598990083986 * 52 = 8250.631474843673. +# Applicable incomes: 10000 - 52 * (single 5 / lone parent 20 / couple 10) +# = 9740 / 8960 / 9480. Tax, premiums, other income held at zero. +# Retained limits: mixed 18-24/under18 incorrectly uses the higher gap amount +# (Sch 2 para 1(3)(f)); young couples' higher own-right/child branches are not +# modelled (para 1(3)(a),(e)). These rate-policy issues are outside this change. +- name: IS single claimant aged 17 is not their own child + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 17 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + benunits: + family: + members: + - claimant + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 4160.635221475105 + income_support_applicable_income: 9740 +- name: IS lone parent aged 17 uses the under-18 allowance + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 17 + is_parent: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + child: + age: 1 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - child + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 4160.635221475105 + income_support_applicable_income: 8960 +- name: IS lone parent aged 20 with qualifying young person uses parent allowance + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 20 + is_parent: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + young_person: + age: 18 + is_in_non_advanced_education: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - young_person + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 5252.007278721122 + income_support_applicable_income: 8960 +- name: IS 17-year-old outside education or training is not a legacy young person + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 20 + is_parent: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + young_person: + age: 17 + is_in_non_advanced_education: false + is_in_approved_training: false + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - young_person + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 4160.635221475105 + income_support_applicable_income: 9740 +- name: IS couple with under-18 partner uses retained mixed-age allowance + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 30 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - partner + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 5252.007278721122 + income_support_applicable_income: 9480 +- name: IS couple aged 24 and 17 retains existing mixed-age approximation + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 24 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - partner + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 5252.007278721122 + income_support_applicable_income: 9480 +- name: IS couple both under 18 uses retained young-couple allowance + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 17 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - partner + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 4160.635221475105 + income_support_applicable_income: 9480 +- name: IS couple with qualifying 19-year-old gets only couple disregard + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + employment_income: 10000 + partner: + age: 40 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + young_person: + age: 19 + is_in_non_advanced_education: true + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + - partner + - young_person + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 8250.631474843673 + income_support_applicable_income: 9480 +- name: IS single claimant age 24 and 25 allowance boundary + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 24 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 4160.635221475105 +- name: IS single claimant aged 25 uses older allowance + period: 2025 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 25 + income_tax: 0 + national_insurance: 0 + social_security_income: 0 + pension_contributions: 0 + benunits: + family: + members: + - claimant + benefits_premiums: 0 + income_support_tariff_income: 0 + output: + income_support_applicable_amount: 5252.007278721122 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_premiums.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_premiums.yaml new file mode 100644 index 0000000000..141319853e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_premiums.yaml @@ -0,0 +1,168 @@ +# IS Schedule 2 paragraphs 11-13A and HB Schedule 3 paragraphs 12-15 +# distinguish the claimant and partner from their children and young persons. +# Annual amounts use the 2026 weekly disability-premia parameters times 52: +# single: disability 44.85 * 52 = 2332.20; enhanced 22 * 52 = 1144; +# severe 86.05 * 52 = 4474.60. +# couple: disability 64 * 52 = 3328; enhanced 31.40 * 52 = 1632.80; +# severe 172.10 * 52 = 8949.20. + +- name: Disabled 16-year-old claimant receives the single legacy premiums + period: 2026 + absolute_error_margin: 0.01 + input: + age: 16 + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + output: + # The claimant qualifies in their own right: no statutory age-18 floor. + disability_premium: 2332.20 + enhanced_disability_premium: 1144 + severe_disability_premium: 4474.60 + +- name: Disabled 17-year-old claimant receives the single legacy premiums + period: 2026 + absolute_error_margin: 0.01 + input: + age: 17 + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + output: + disability_premium: 2332.20 + enhanced_disability_premium: 1144 + severe_disability_premium: 4474.60 + +- name: Disabled 18-year-old young person does not qualify a parent for legacy premiums + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 18 + is_in_non_advanced_education: true + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + family: + members: [parent, young_person] + output: + # Zero qualifying claimants/partners, so each claimant premium is zero. + # The separate disabled-child premium is outside the current model. + disability_premium: 0 + enhanced_disability_premium: 0 + severe_disability_premium: 0 + +- name: Disabled 19-year-old young person does not qualify a parent for legacy premiums + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 19 + is_in_non_advanced_education: true + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + family: + members: [parent, young_person] + output: + disability_premium: 0 + enhanced_disability_premium: 0 + severe_disability_premium: 0 + +- name: Disabled 17-year-old partner qualifies the couple for legacy premiums + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + family: + members: [claimant, partner] + output: + disability_premium: 3328 + enhanced_disability_premium: 1632.80 + # Deliberately preserve the existing severe-premium approximation: + # one qualifying partner currently triggers the full couple rate. + # IS Sch 2 para 13 / HB Sch 3 para 14 require both to qualify. + severe_disability_premium: 8949.20 + +- name: Disabled lone parent with a young person keeps single legacy premiums + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + young_person: + age: 19 + is_in_non_advanced_education: true + benunits: + family: + members: [parent, young_person] + output: + disability_premium: 2332.20 + enhanced_disability_premium: 1144 + severe_disability_premium: 4474.60 + +- name: Disabled child does not qualify a parent for legacy claimant premiums + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 15 + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + family: + members: [parent, child] + output: + disability_premium: 0 + enhanced_disability_premium: 0 + severe_disability_premium: 0 + +- name: Two qualifying partners receive one couple rate for each legacy premium + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + partner: + age: 32 + is_disabled_for_benefits: true + is_enhanced_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + family: + members: [claimant, partner] + output: + disability_premium: 3328 + enhanced_disability_premium: 1632.80 + severe_disability_premium: 8949.20 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_youngest_child.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_youngest_child.yaml new file mode 100644 index 0000000000..b6bbe5369a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/legacy_youngest_child.yaml @@ -0,0 +1,95 @@ +# SSCBA 1992 ss.137(1),142(1): children are under 16, excluding the claimant +# and partner. IS Sch 1B para 1 says under 5; the model deliberately retains +# <= 5 pending the separate threshold decision requested in the brief. +- name: IS youngest child excludes a young claimant and qualifying young person + period: 2025 + input: + people: + claimant: + age: 17 + is_parent: true + is_benunit_head: true + income_support_reported: 1000 + child: + age: 4 + young_person: + age: 18 + is_in_non_advanced_education: true + benunits: + family: + members: [claimant, child, young_person] + esa_income: 0 + income_support_assessable_capital: 0 + output: + youngest_child_age_for_legacy_benefits: 4 + income_support_eligible: true + +- name: IS youngest child threshold retains age five and excludes age six + period: 2025 + input: + people: + claimant1: + age: 30 + income_support_reported: 1000 + child1: + age: 5 + claimant2: + age: 30 + income_support_reported: 1000 + child2: + age: 6 + benunits: + family1: + members: [claimant1, child1] + esa_income: 0 + income_support_assessable_capital: 0 + family2: + members: [claimant2, child2] + esa_income: 0 + income_support_assessable_capital: 0 + output: + youngest_child_age_for_legacy_benefits: [5, 6] + income_support_eligible: [true, false] + +- name: IS young person alone does not satisfy young-child eligibility + period: 2025 + input: + people: + claimant: + age: 30 + is_parent: true + income_support_reported: 1000 + young_person: + age: 16 + is_in_non_advanced_education: true + benunits: + family: + members: [claimant, young_person] + esa_income: 0 + income_support_assessable_capital: 0 + output: + is_lone_parent: true + income_support_eligible: false + +- name: IS claimant with under-18 partner is not a lone parent + period: 2025 + input: + people: + claimant: + age: 30 + is_parent: true + income_support_reported: 1000 + partner: + age: 17 + is_parent: true + child: + age: 3 + benunits: + family: + members: [claimant, partner, child] + esa_income: 0 + income_support_assessable_capital: 0 + output: + youngest_child_age_for_legacy_benefits: 3 + is_lone_parent: false + income_support_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support_post_migration.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support_post_migration.yaml index 0223228b7a..cc897fc829 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support_post_migration.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support_post_migration.yaml @@ -8,7 +8,6 @@ income_support_reported: 3000 c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -30,7 +29,6 @@ income_support_reported: 3000 c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] @@ -52,7 +50,6 @@ income_support_reported: 3000 c1: age: 4 - is_child: true benunits: b1: members: [p1, c1] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_capital.yaml index 13ecc3d659..024036709e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_capital.yaml @@ -98,7 +98,7 @@ jsa_income_eligible: false jsa_income: 0 -- name: Income-based JSA allocates household capital by adult share when nobody reports the award +- name: Income-based JSA allocates household capital by claimant and partner share when nobody reports the award period: 2026 absolute_error_margin: 0 input: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/legacy_capital.yaml new file mode 100644 index 0000000000..58fb386941 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/legacy_capital.yaml @@ -0,0 +1,77 @@ +# https://www.legislation.gov.uk/uksi/1996/207/regulation/88 +# Only claimant and partner contribute capital weights. The equal allocation of +# household stocks is a PolicyEngine convention, not a statutory allocation rule. +- name: Income-based JSA capital allocation includes a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # Two claimants: each gets 18,000 * 1 / 2 = 9,000. + # Previously the under-18 claimant had zero weight: [0, 18,000]. + jsa_income_assessable_capital: [9_000, 9_000] + +- name: Income-based JSA capital allocation excludes a dependent student aged 19 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_parent: true + student: + age: 19 + is_in_non_advanced_education: true + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant, student] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, student, unrelated_claimant] + savings: 18_000 + output: + # The dependent student adds no weight. Each claimant gets 18,000 / 2. + # Previously the student's unit got 18,000 * 2 / 3 = 12,000 (other: 6,000). + jsa_income_assessable_capital: [9_000, 9_000] + +- name: Income-based JSA claim path recognises a claimant aged 17 + period: 2021 + absolute_error_margin: 0 + input: + people: + claimant: + age: 17 + jsa_income_reported: 1_000 + unrelated_claimant: + age: 30 + benunits: + claimant_benunit: + members: [claimant] + unrelated_benunit: + members: [unrelated_claimant] + households: + household: + members: [claimant, unrelated_claimant] + savings: 18_000 + output: + # The model's claim-path convention assigns all 18,000 to the claimant + # and zero to the non-claiming unit. Previously the age-18 gate yielded + # fallback allocation [0, 18,000] despite this claimant's claim path. + jsa_income_assessable_capital: [18_000, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_claimants_and_children.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_claimants_and_children.yaml new file mode 100644 index 0000000000..0758d27e7d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits_claimants_and_children.yaml @@ -0,0 +1,184 @@ +# Working Tax Credit: who is the claimant, who is a child or qualifying young +# person (QYP), and which children count for the childcare element. +# +# Claims are single or joint by the members of a couple (Tax Credits Act 2002 +# s.3); a claimant is responsible for children and QYPs under the Child Tax +# Credit rules (SI 2002/2007 regs 3 and 5). Dependants are never claimants. +# 2020-21 elements (SI 2002/2005 Sch 2): lone parent 2,045; couple 2,045; +# disabled 3,220; severe disability 1,390. Hours: 16 for lone parents and +# disabled workers, 24 for a couple with children (one working 16+), 30 at 25+. + +- name: A couple with an 18-year-old QYP and a 10-year-old needs 24 hours, not 16 + # Previously three members aged 18+ made this a "lone parent" family, eligible + # at 16 hours with both the lone parent and couple elements. + period: 2020 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 35 + is_parent: true + weekly_hours: 20 + working_tax_credit_reported: true + parent_2: + age: 33 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + child: + age: 10 + benunits: + benunit: + members: [parent_1, parent_2, student, child] + output: + is_WTC_eligible: false + +- name: A couple with an 18-year-old QYP working 26 hours gets the couple element only + period: 2020 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 35 + is_parent: true + weekly_hours: 26 + working_tax_credit_reported: true + parent_2: + age: 33 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent_1, parent_2, student] + output: + is_WTC_eligible: true + WTC_couple_element: 2_045 + WTC_lone_parent_element: 0 + +- name: A lone parent whose only dependant is an 18-year-old QYP qualifies on 16 hours + # Previously the 18-year-old made this a childless "couple" needing 30 hours. + period: 2020 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + weekly_hours: 20 + working_tax_credit_reported: true + student: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, student] + output: + is_WTC_eligible: true + WTC_lone_parent_element: 2_045 + WTC_couple_element: 0 + +- name: A childless 17-year-old is not a lone parent and needs to be 25 for the 30-hour route + period: 2020 + absolute_error_margin: 0 + input: + people: + worker: + age: 17 + weekly_hours: 30 + working_tax_credit_reported: true + benunits: + benunit: + members: [worker] + output: + is_WTC_eligible: false + +- name: A severely disabled 18-year-old QYP gives no WTC severe disability element + # The element is for the claimant or a joint claimant (reg 17); the young + # person's severe disability counts for Child Tax Credit instead. + period: 2020 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 45 + is_parent: true + weekly_hours: 30 + working_tax_credit_reported: true + parent_2: + age: 44 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + is_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + weekly_hours: 16 + benunits: + benunit: + members: [parent_1, parent_2, student] + output: + is_WTC_eligible: true + WTC_severely_disabled_element: 0 + WTC_disabled_element: 0 + +- name: A severely disabled lone claimant gets the WTC severe disability element + period: 2020 + absolute_error_margin: 0 + input: + people: + worker: + age: 30 + weekly_hours: 30 + working_tax_credit_reported: true + is_severely_disabled_for_benefits: true + benunits: + benunit: + members: [worker] + output: + WTC_severely_disabled_element: 1_390 + +- name: Childcare element counts children until the September after their 15th birthday + # A 16-year-old is past the reg 14(3) limit, so only the 10-year-old counts: + # the one-child cap applies. Cap 248.63997060 x 52 = 12,929.28; 70% of that + # is 9,050.50 (the 20,000 of charges exceed the cap). Previously the + # 16-year-old counted and the two-child cap applied. + period: 2020 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + weekly_hours: 20 + working_tax_credit_reported: true + childcare_expenses: 20_000 + teenager: + age: 16 + child: + age: 10 + benunits: + benunit: + members: [parent, teenager, child] + output: + is_child_for_working_tax_credit_childcare_element: [false, false, true] + WTC_childcare_element: 9_050.50 + +- name: Childcare element counts a disabled child until the September after their 16th birthday + period: 2020 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + teenager: + age: 16 + is_disabled_for_benefits: true + benunits: + benunit: + members: [parent, teenager] + output: + is_child_for_working_tax_credit_childcare_element: [false, true] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition.yaml index 5e88f76b19..55a589cd02 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition.yaml @@ -4,6 +4,7 @@ input: people: person: + age: 30 in_work: false output: uc_childcare_work_condition: false @@ -14,6 +15,7 @@ input: people: person: + age: 30 in_work: true output: uc_childcare_work_condition: true @@ -25,10 +27,10 @@ people: person1: in_work: false - is_adult: true + age: 40 person2: in_work: true - is_adult: false + age: 17 output: uc_childcare_work_condition: false @@ -39,10 +41,10 @@ people: person1: in_work: false - is_adult: true + age: 40 person2: in_work: true - is_adult: true + age: 39 benunits: benunit: members: [person1, person2] @@ -56,12 +58,149 @@ people: person1: in_work: true - is_adult: true + age: 40 person2: in_work: true - is_adult: true + age: 39 benunits: benunit: members: [person1, person2] output: uc_childcare_work_condition: true + +- name: Nonworking dependent aged 18 does not block a working claimant's childcare + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + in_work: true + young_person: + age: 18 + is_in_non_advanced_education: true + in_work: false + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, young_person, child] + output: + # Reg. 32 tests claimant/partner work, not the dependent young person's: + # https://www.legislation.gov.uk/uksi/2013/376/regulation/32 + # Reg. 34: min(5,000 * 85%, 1,031.88 * 12) = 4,250, formerly zero. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/34 + is_uc_claimant: [true, false, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Nonworking dependent aged 19 does not block working joint claimants' childcare + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + in_work: true + partner: + age: 39 + in_work: true + young_person: + age: 19 + is_in_non_advanced_education: true + in_work: false + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, partner, young_person, child] + output: + # Reg. 32: both claimants work; the dependent young person is excluded. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/32 + # Reg. 34: min(5,000 * 85%, 1,031.88 * 12) = 4,250, formerly zero. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/34 + is_uc_claimant: [true, true, false, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Working claimant aged 16 satisfies the childcare work condition + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 16 + is_benunit_head: true + is_parent: true + in_work: true + child: + age: 1 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, child] + output: + # Reg. 32: the single claimant works; age 18 is not a work condition. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/32 + # Reg. 34: min(5,000 * 85%, 1,031.88 * 12) = 4,250, formerly zero. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/34 + is_uc_claimant: [true, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Working claimant aged 17 satisfies the childcare work condition + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 17 + is_benunit_head: true + is_parent: true + in_work: true + child: + age: 1 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, child] + output: + # Reg. 32: the single claimant works; age 18 is not a work condition. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/32 + # Reg. 34: min(5,000 * 85%, 1,031.88 * 12) = 4,250, formerly zero. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/34 + is_uc_claimant: [true, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: An explicitly identified nonworking partner aged 17 fails the work condition + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 20 + is_parent: true + is_uc_claimant: true + in_work: true + partner: + age: 17 + is_uc_claimant: true + in_work: false + child: + age: 1 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, partner, child] + output: + # Reg. 32(1)(b) applies to a partner regardless of age. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/32 + # No partner exception is modelled: failed work condition * 5,000 * 85% + # = 0, formerly 4,250 when the partner was excluded by the age-18 flag. + uc_childcare_work_condition: false + uc_childcare_element: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml index c57eb5efa5..e9eee500eb 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml @@ -126,9 +126,11 @@ uc_work_allowance: 0 universal_credit: 5_970.60 -# The existing eligibility formula excludes all under-18 claimants. This case -# fixes claimant classification only; it does not implement the legal exceptions. -- name: Case 5, an under-18 parent is not a couple because adult count is zero. +# UC Regulations 2013 reg.8(1)(d) lowers the minimum age to 16 for a claimant +# responsible for a child: https://www.legislation.gov.uk/uksi/2013/376/regulation/8 +# This deliberate change admits the 17-year-old lone parent previously excluded. +# 2025 UC = (316.98 standard allowance + 292.81 child element) * 12 = 7,317.48. +- name: Case 5, a 17-year-old lone parent meets the reduced UC minimum age. period: 2025 absolute_error_margin: 0.01 input: @@ -151,7 +153,9 @@ output: uc_standard_allowance_claimant_type: SINGLE_YOUNG uc_standard_allowance: 3_803.76 - universal_credit: 0 + is_uc_eligible: true + uc_child_element: 3_513.72 + universal_credit: 7_317.48 - name: Case 6, retained nonqualifying child roles do not imply a partner. period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml index 3647f0e3d4..62a0fd3051 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml @@ -5,7 +5,8 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 10 - benunit_tax: 1 + # The claimant's own tax (UC reg 22 counts the claimant's earnings). + tax: 1 output: uc_earned_income: 1100 - 11 @@ -16,6 +17,7 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 2_000 - benunit_tax: 1 + # The claimant's own tax (UC reg 22 counts the claimant's earnings). + tax: 1 output: uc_earned_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_UC_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_UC_eligible.yaml index fa145e20c1..926ade283d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_UC_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_UC_eligible.yaml @@ -1,4 +1,4 @@ -- name: Has no working-age adults +- name: Has no claimants meeting the age conditions period: 2020 absolute_error_margin: 0 input: @@ -13,7 +13,7 @@ output: is_uc_eligible: false -- name: Has one working-age adult +- name: Has one claimant meeting the age conditions period: 2020 absolute_error_margin: 0 input: @@ -28,7 +28,7 @@ output: is_uc_eligible: true -- name: Working-age adult remains eligible at the upper capital limit +- name: Claimant meeting the age conditions remains eligible at the upper capital limit period: 2025 absolute_error_margin: 0 input: @@ -45,7 +45,7 @@ output: is_uc_eligible: true -- name: Working-age adult is ineligible above the upper capital limit +- name: Claimant meeting the age conditions is ineligible above the upper capital limit period: 2025 absolute_error_margin: 0 input: @@ -105,7 +105,7 @@ uc_assessable_capital: [10_000, 10_000] is_uc_eligible: [true, true] -- name: Multi-benunit households split proxy capital equally across adults +- name: Multi-benunit households split proxy capital equally across claimants period: 2025 absolute_error_margin: 0 input: @@ -174,7 +174,7 @@ uc_assessable_capital: [12_000, 8_000] is_uc_eligible: [true, true] -- name: Multi-benunit proxy capital is weighted by adults in each benunit +- name: Multi-benunit proxy capital is weighted by claimants in each benunit period: 2025 absolute_error_margin: 0 input: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_claimant.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_claimant.yaml index 314fce3fe1..b3baa75c28 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_claimant.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_claimant.yaml @@ -77,7 +77,7 @@ output: is_uc_claimant: true -- name: Case 6, an older member without qualifying education remains an adult fallback. +- name: Case 6, an adult child without qualifying education is neither a partner nor a UC child. period: 2025 input: people: @@ -90,6 +90,9 @@ benunit: members: [person1, person2] output: - # The default education input does not establish an older dependent QYP. + # The default (higher) education input does not establish a UC qualifying + # young person. An 18-year-old 22 years younger than the head is presumed + # the parent's child, not their partner, so the parent claims alone + # (previously the two were treated as joint claimants). is_child_or_qualifying_young_person_for_universal_credit: [false, false] - is_uc_claimant: [true, true] + is_uc_claimant: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/meets_uc_minimum_age_condition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/meets_uc_minimum_age_condition.yaml new file mode 100644 index 0000000000..0a3e3288f9 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/meets_uc_minimum_age_condition.yaml @@ -0,0 +1,217 @@ +# WRA 2012 s.4(1)(a): standard minimum age = 18. +# UC Regulations 2013 reg.8(1): reduced minimum age = 16. +# https://www.legislation.gov.uk/ukpga/2012/5/section/4 +# https://www.legislation.gov.uk/uksi/2013/376/regulation/8 + +- name: Standard UC minimum age starts at 18 + period: 2025 + input: + age: [15, 16, 17, 18, 19] + output: + # No exception applies: age >= 18. + meets_uc_minimum_age_condition: [false, false, false, true, true] + +- name: LCWRA proxy permits 16 and 17 year old claimants + period: 2025 + input: + age: [15, 16, 17, 18] + uc_limited_capability_for_WRA: [true, true, true, true] + uc_reported_capital: [0, 0, 0, 0] + output: + # Reg.8(1)(a)-(b) proxy: true only once age >= 16. + meets_uc_minimum_age_condition: [false, true, true, true] + is_uc_eligible: [false, true, true, true] + +- name: Caring hours proxy starts at 35 hours for age 16 and over + period: 2025 + input: + age: [15, 16, 16, 17, 17] + care_hours: [35, 34, 35, 34, 35] + uc_reported_capital: [0, 0, 0, 0, 0] + output: + # Reg.8(1)(c) proxy: (age >= 16) AND (hours >= 35). + meets_uc_minimum_age_condition: [false, false, true, false, true] + is_uc_eligible: [false, false, true, false, true] + +- name: Receipt of a carer benefit also meets the caring proxy + period: 2025 + input: + age: 16 + care_hours: 0 + receives_carer_benefit: true + uc_reported_capital: 0 + output: + # Reg.8(1)(c) proxy: age >= 16 AND carer benefit receipt. + meets_uc_minimum_age_condition: true + is_uc_eligible: true + +- name: A 16 year old claimant responsible for a child meets the reduced age + period: 2025 + input: + people: + claimant: + age: 16 + is_benunit_head: true + child: + age: 1 + is_benunit_head: false + benunits: + family: + members: [claimant, child] + uc_reported_capital: 0 + output: + # Reg.8(1)(d): claimant age 16 >= 16 and nonclaimant child age 1 < 16. + is_uc_claimant: [true, false] + meets_uc_minimum_age_condition: [true, false] + is_uc_eligible: true + +- name: A 15 year old parent remains below the reduced minimum age + period: 2025 + input: + people: + claimant: + age: 15 + is_uc_claimant: true + child: + age: 1 + is_uc_claimant: false + benunits: + family: + members: [claimant, child] + uc_reported_capital: 0 + output: + # Even with responsibility, 15 < 16. + meets_uc_minimum_age_condition: [false, false] + is_uc_eligible: false + +- name: Responsibility for a child aged 15 still meets reg 8 paragraph d + period: 2025 + input: + people: + claimant: + age: 17 + is_uc_claimant: true + child: + age: 15 + is_uc_claimant: false + benunits: + family: + members: [claimant, child] + uc_reported_capital: 0 + output: + # Responsibility need not mean biological parenthood: 17 >= 16 and + # the nonclaimant aged 15 is still below the child age limit of 16. + meets_uc_minimum_age_condition: [true, false] + is_uc_eligible: true + +- name: Responsibility for a qualifying young person alone is not reg 8 paragraph d + period: 2025 + input: + people: + claimant: + age: 17 + is_uc_claimant: true + young_person: + age: 16 + is_uc_claimant: false + is_in_non_advanced_education: true + benunits: + family: + members: [claimant, young_person] + uc_reported_capital: 0 + output: + # Reg.8(1)(d) says child (age < 16), so age 16 does not qualify. + meets_uc_minimum_age_condition: [false, false] + is_uc_eligible: false + +- name: A claimant cannot count another claimant as a dependent child + period: 2025 + input: + people: + claimant: + age: 17 + is_uc_claimant: true + partner: + age: 15 + is_uc_claimant: true + benunits: + family: + members: [claimant, partner] + uc_reported_capital: 0 + output: + # There are zero nonclaimant children; neither claimant meets age 18. + meets_uc_minimum_age_condition: [false, false] + is_uc_eligible: false + +- name: An under 18 claimant cannot gain eligibility from an older dependent student + period: 2025 + input: + people: + claimant: + age: 17 + is_benunit_head: true + is_parent: true + young_person: + age: 18 + is_benunit_head: false + is_in_non_advanced_education: true + benunits: + family: + members: [claimant, young_person] + uc_reported_capital: 0 + output: + # The claimant is under 18 with no child under 16; the 18-year-old is + # not a claimant. Zero claimants satisfy the age conditions. + is_uc_claimant: [true, false] + meets_uc_minimum_age_condition: [false, true] + is_uc_eligible: false + +- name: A pension age claimant cannot gain eligibility from an 18 year old dependant + period: 2025 + input: + people: + claimant: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 18 + is_benunit_head: false + is_in_non_advanced_education: true + benunits: + family: + members: [claimant, young_person] + uc_reported_capital: 0 + output: + # WRA ss.3-4 apply to claimants: 70 >= SPA; age 18 is a nonclaimant. + # Previously the dependent student's age alone made this unit eligible. + is_uc_claimant: [true, false] + is_uc_eligible: false + +- name: Mixed age couple retains eligibility through its younger claimant + period: 2025 + input: + people: + older: + age: 70 + younger: + age: 60 + benunits: + couple: + members: [older, younger] + uc_reported_capital: 0 + output: + # Reg.3(2)(a): one claimant is age 60, between minimum age 18 and SPA. + is_uc_claimant: [true, true] + is_uc_eligible: true + +- name: Reduced minimum age does not bypass the capital limit + period: 2025 + input: + age: [16, 17] + uc_limited_capability_for_WRA: [true, true] + uc_reported_capital: [16_000, 16_001] + output: + # Both ages >= 16; only 16,000 <= the 16,000 capital limit. + meets_uc_minimum_age_condition: [true, true] + is_uc_eligible: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/test_uc_capital_allocation.py b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/test_uc_capital_allocation.py new file mode 100644 index 0000000000..30c50614f6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/test_uc_capital_allocation.py @@ -0,0 +1,96 @@ +"""The household capital proxy conserves residual capital within each household.""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings, strategies as st +from policyengine_core.simulations import Simulation + +from policyengine_uk.system import system + + +@st.composite +def household_capital_situations(draw): + household_specs = draw( + st.lists( + st.tuples( + st.integers(min_value=0, max_value=200_000), + st.lists( + st.tuples( + st.integers(min_value=1, max_value=2), + st.integers(min_value=0, max_value=2), + st.booleans(), + st.integers(min_value=0, max_value=50_000), + ), + min_size=1, + max_size=4, + ), + ), + min_size=1, + max_size=3, + ) + ) + people, benunits, households, expected = {}, {}, {}, [] + for household_index, (capital, unit_specs) in enumerate(household_specs): + household_members = [] + unreported_indices = [] + reported_values = {} + for unit_index, ( + claimant_count, + dependant_count, + has_reported, + reported_capital, + ) in enumerate(unit_specs): + # Every generated household has at least one unreported unit. + has_reported = has_reported and unit_index > 0 + unit_id = f"household_{household_index}_unit_{unit_index}" + members = [] + for person_index in range(claimant_count + dependant_count): + person_id = f"{unit_id}_person_{person_index}" + is_claimant = person_index < claimant_count + people[person_id] = { + "age": {2025: 17 if is_claimant else 19}, + "is_uc_claimant": {2025: is_claimant}, + "is_benunit_head": {2025: person_index == 0}, + } + members.append(person_id) + if has_reported: + reported_values[len(benunits)] = reported_capital + else: + unreported_indices.append(len(benunits)) + benunits[unit_id] = { + "members": members, + "uc_reported_capital": {2025: reported_capital if has_reported else -1}, + } + household_members.extend(members) + households[f"household_{household_index}"] = { + "members": household_members, + "savings": {2025: capital}, + } + expected.append( + ( + unreported_indices, + reported_values, + max(0, capital - sum(reported_values.values())), + ) + ) + return {"people": people, "benunits": benunits, "households": households}, expected + + +# Test conservation independently of machine-dependent generation timings. +@settings( + max_examples=30, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(household_capital_situations()) +def test_unreported_capital_shares_sum_to_each_households_residual(case): + situation, expected = case + # Reuse baseline rules; each example still gets fresh people and inputs. + simulation = Simulation(tax_benefit_system=system, situation=situation) + assessed_capital = simulation.calculate("uc_assessable_capital", 2025) + + assert np.isfinite(assessed_capital).all() + for unreported_indices, reported_values, residual in expected: + assert assessed_capital[unreported_indices].sum() == pytest.approx(residual) + for index, reported_capital in reported_values.items(): + assert assessed_capital[index] == pytest.approx(reported_capital) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_assessable_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_assessable_capital.yaml new file mode 100644 index 0000000000..99e1fbfc52 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_assessable_capital.yaml @@ -0,0 +1,165 @@ +# WRA 2012 s.5 assesses the claimant's or joint claimants' capital; +# UC Regulations 2013 reg.18(2) includes a single claimant's partner's capital. +# https://www.legislation.gov.uk/ukpga/2012/5/section/5 +# https://www.legislation.gov.uk/uksi/2013/376/regulation/18 +# Allocating unreported household capital by claimant/partner count is a +# PolicyEngine convention, not a statutory allocation rule. + +- name: A qualifying young person does not increase their parents' capital share. + period: 2025 + input: + people: + parent: + age: 40 + is_benunit_head: true + is_parent: true + partner: + age: 39 + is_benunit_head: false + is_parent: true + qualifying_young_person: + age: 18 + is_benunit_head: false + is_in_non_advanced_education: true + other_claimant: + age: 25 + is_benunit_head: true + benunits: + parents: + members: [parent, partner, qualifying_young_person] + other: + members: [other_claimant] + households: + household: + members: [parent, partner, qualifying_young_person, other_claimant] + savings: 36_000 + output: + is_uc_claimant: [true, true, false, true] + household_uc_unreported_claimants: 3 + # Previously 36,000 * 3/4 = 27,000 and 36,000 * 1/4 = 9,000. + # Claimant weights give 36,000 * 2/3 = 24,000 and 36,000 / 3 = 12,000. + uc_assessable_capital: [24_000, 12_000] + +- name: An under-18 claimant receives the full capital proxy in their household. + period: 2025 + input: + people: + claimant: + age: 17 + is_benunit_head: true + is_parent: true + child: + age: 1 + is_benunit_head: false + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + savings: 20_000 + output: + is_uc_claimant: [true, false] + household_uc_unreported_claimants: 1 + # Previously zero; the claimant now receives 20,000 * 1/1 = 20,000. + uc_assessable_capital: 20_000 + +- name: Under-18 and older claimants receive equal unreported capital shares. + period: 2025 + input: + people: + young_claimant: + age: 16 + is_benunit_head: true + older_claimant: + age: 30 + is_benunit_head: true + benunits: + young: + members: [young_claimant] + older: + members: [older_claimant] + households: + household: + members: [young_claimant, older_claimant] + savings: 36_000 + output: + household_uc_unreported_claimants: 2 + # Previously [0, 36,000]; each now receives 36,000 * 1/2 = 18,000. + uc_assessable_capital: [18_000, 18_000] + +- name: Reported capital leaves a residual split between unreported claimants. + period: 2025 + input: + people: + reported_parent: + age: 40 + is_benunit_head: true + is_parent: true + qualifying_young_person: + age: 19 + is_benunit_head: false + is_in_non_advanced_education: true + young_claimant: + age: 17 + is_benunit_head: true + older_claimant: + age: 30 + is_benunit_head: true + partner: + age: 31 + is_benunit_head: false + benunits: + reported: + members: [reported_parent, qualifying_young_person] + uc_reported_capital: 8_000 + young: + members: [young_claimant] + couple: + members: [older_claimant, partner] + households: + household: + members: [reported_parent, qualifying_young_person, young_claimant, older_claimant, partner] + savings: 32_000 + output: + household_uc_reported_capital: 8_000 + household_uc_unreported_claimants: 3 + # 32,000 - 8,000 = 24,000 residual. Shares are 24,000 / 3 = 8,000 + # and 24,000 * 2/3 = 16,000. Previously [8,000, 0, 24,000]. + uc_assessable_capital: [8_000, 8_000, 16_000] + +- name: An explicit zero capital report excludes that unit from the proxy. + period: 2025 + input: + people: + reported_claimant: + age: 40 + is_benunit_head: true + unreported_claimant: + age: 30 + is_benunit_head: true + benunits: + reported: + members: [reported_claimant] + uc_reported_capital: 0 + unreported: + members: [unreported_claimant] + households: + household: + members: [reported_claimant, unreported_claimant] + savings: 12_000 + output: + household_uc_unreported_claimants: 1 + # 12,000 - 0 = 12,000 residual, allocated entirely to the unreported unit. + uc_assessable_capital: [0, 12_000] + +- name: A situation with no claimant roles has a zero proxy without division by zero. + period: 2025 + input: + age: 17 + is_uc_claimant: false + savings: 12_000 + output: + household_uc_unreported_claimants: 0 + # No claimant weights means no allocation: guarded zero instead of 0/0. + uc_assessable_capital: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/adult_dependants_grant/adult_dependants_grant.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/adult_dependants_grant/adult_dependants_grant.yaml index f54ef1f943..11cedc695c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/adult_dependants_grant/adult_dependants_grant.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/adult_dependants_grant/adult_dependants_grant.yaml @@ -209,3 +209,26 @@ output: adult_dependants_grant_eligible: [false, false] adult_dependants_grant: [0, 0] + +# The partner route needs a partner (SI 2011/1986 reg 44(2)(a)); an older child +# still at school is a dependant, not a partner. +- name: A lone parent with an 18-year-old at school has no partner for Adult Dependants' Grant + period: 2025 + input: + people: + student: + age: 30 + is_parent: true + teenager: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [student, teenager] + households: + household: + members: [student, teenager] + country: ENGLAND + output: + is_couple: false + adult_dependants_grant_adult_eligible: [false, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml index 26726e4efa..33600f00ac 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml @@ -134,7 +134,6 @@ pip_dl_category: STANDARD sibling: age: 10 - is_child: true benunits: benunit: members: ["parent", "student", "sibling"] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/care_to_learn/care_to_learn_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/care_to_learn/care_to_learn_eligible.yaml index 88966209eb..1628789ff6 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/care_to_learn/care_to_learn_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/care_to_learn/care_to_learn_eligible.yaml @@ -9,7 +9,6 @@ is_apprentice: false child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -31,7 +30,6 @@ is_apprentice: false child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -72,7 +70,6 @@ is_apprentice: false child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -94,7 +91,6 @@ is_apprentice: true child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -116,7 +112,6 @@ is_apprentice: false child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -138,10 +133,8 @@ is_apprentice: false child1: age: 2 - is_child: true child2: age: 4 - is_child: true benunits: benunit: members: ["parent", "child1", "child2"] @@ -163,7 +156,6 @@ is_apprentice: false child: age: 2 - is_child: true benunits: benunit: members: ["parent", "child"] @@ -172,4 +164,46 @@ members: ["parent", "child"] country: ENGLAND output: - care_to_learn_eligible: [false, false] \ No newline at end of file + care_to_learn_eligible: [false, false] + +# The young parent must receive Child Benefit for the child they claim for +# (ESFA conditions of grant), so the parent can never count as their own child. +- name: Care to Learn - A 17-year-old parent with no child in the benefit unit + period: 2024 + input: + people: + parent: + age: 17 + is_parent: true + current_education: UPPER_SECONDARY + is_apprentice: false + benunits: + benunit: + members: ["parent"] + households: + household: + members: ["parent"] + country: ENGLAND + output: + care_to_learn_eligible: false + +- name: Care to Learn - A 17-year-old parent with a baby + period: 2024 + input: + people: + parent: + age: 17 + is_parent: true + current_education: UPPER_SECONDARY + is_apprentice: false + baby: + age: 0 + benunits: + benunit: + members: ["parent", "baby"] + households: + household: + members: ["parent", "baby"] + country: ENGLAND + output: + care_to_learn_eligible: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml index ef00a4b516..760c906392 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml @@ -10,7 +10,6 @@ childcare_expenses: 5_000 child: age: 4 - is_child: true benunits: benunit: members: ["student", "child"] @@ -35,7 +34,6 @@ childcare_expenses: 20_000 child: age: 3 - is_child: true benunits: benunit: members: ["student", "child"] @@ -59,10 +57,8 @@ childcare_expenses: 25_000 child_1: age: 6 - is_child: true child_2: age: 8 - is_child: true benunits: benunit: members: ["student", "child_1", "child_2"] @@ -86,7 +82,6 @@ childcare_grant_household_income: 20_107.23 child: age: 2 - is_child: true benunits: benunit: members: ["student", "child"] @@ -110,7 +105,6 @@ childcare_expenses: 4_000 child: age: 16 - is_child: true childcare_grant_child_has_special_educational_needs: true benunits: benunit: @@ -135,7 +129,6 @@ childcare_expenses: 4_000 child: age: 16 - is_child: true benunits: benunit: members: ["student", "child"] @@ -158,7 +151,6 @@ childcare_expenses: 4_000 child: age: 16 - is_child: true is_disabled_for_benefits: true benunits: benunit: @@ -183,7 +175,6 @@ childcare_expenses: 6_000 child: age: 4 - is_child: true benunits: benunit: members: ["student", "child"] @@ -210,7 +201,6 @@ childcare_grant_receives_postgraduate_loan: true child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] @@ -235,7 +225,6 @@ childcare_grant_receives_nhs_childcare_support: true child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] @@ -258,7 +247,6 @@ childcare_expenses: 6_000 child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] @@ -282,7 +270,6 @@ childcare_expenses: 6_000 child: age: 5 - is_child: true tax_free_childcare: 1_000 benunits: benunit: @@ -308,7 +295,6 @@ childcare_grant_provider_is_nanny: true child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] @@ -335,7 +321,6 @@ working_tax_credit_reported: 1 child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.yaml index 9b979a3893..53f74422a2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.yaml @@ -7,15 +7,15 @@ country: ENGLAND people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: true parent2: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: false child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: @@ -32,15 +32,15 @@ country: SCOTLAND people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: true parent2: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: false child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: @@ -57,15 +57,15 @@ country: ENGLAND people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: false extended_childcare_entitlement_work_condition: true parent2: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: false child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: @@ -82,15 +82,15 @@ country: ENGLAND people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: false parent2: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: false child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: @@ -107,11 +107,11 @@ country: ENGLAND people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: true child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: @@ -128,14 +128,70 @@ country: WALES people: parent1: - is_child: false + age: 35 extended_childcare_entitlement_meets_income_requirements: true extended_childcare_entitlement_work_condition: true child1: - is_child: true + age: 3 extended_childcare_entitlement_work_condition: false benunits: benunit1: members: [parent1, child1] output: - extended_childcare_entitlement_eligible: false \ No newline at end of file + extended_childcare_entitlement_eligible: false +# The income conditions apply to the parent and the parent's partner +# (Childcare Act 2016 s.1(2)(d); SI 2022/1134 regs 14, 15 and 18), not to an +# older child still at school. +- name: Eligible - an 18-year-old at school does not have to meet the income test + period: 2025 + input: + households: + household1: + members: [parent1, parent2, student, child1] + country: ENGLAND + people: + parent1: + age: 35 + is_parent: true + extended_childcare_entitlement_meets_income_requirements: true + extended_childcare_entitlement_work_condition: true + parent2: + age: 35 + is_parent: true + extended_childcare_entitlement_meets_income_requirements: true + extended_childcare_entitlement_work_condition: false + student: + age: 18 + current_education: UPPER_SECONDARY + extended_childcare_entitlement_meets_income_requirements: false + extended_childcare_entitlement_work_condition: false + child1: + age: 3 + extended_childcare_entitlement_work_condition: false + benunits: + benunit1: + members: [parent1, parent2, student, child1] + output: + extended_childcare_entitlement_eligible: true + +- name: Not eligible - a 17-year-old parent must meet the income test + period: 2025 + input: + households: + household1: + members: [parent1, child1] + country: ENGLAND + people: + parent1: + age: 17 + is_parent: true + extended_childcare_entitlement_meets_income_requirements: false + extended_childcare_entitlement_work_condition: true + child1: + age: 1 + extended_childcare_entitlement_work_condition: false + benunits: + benunit1: + members: [parent1, child1] + output: + extended_childcare_entitlement_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/maintenance_loans/maintenance_loan.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/maintenance_loans/maintenance_loan.yaml index fa61e7cc17..2c53b50689 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/maintenance_loans/maintenance_loan.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/maintenance_loans/maintenance_loan.yaml @@ -70,7 +70,6 @@ is_parent: true child: age: 1 - is_child: true benunits: benunit: members: ["student", "child"] @@ -432,7 +431,6 @@ is_parent: true child: age: 1 - is_child: true benunits: grandparent_benunit: members: ["grandparent"] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/parents_learning_allowance/parents_learning_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/parents_learning_allowance/parents_learning_allowance.yaml index 858ecc4c78..566e371ab9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/parents_learning_allowance/parents_learning_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/parents_learning_allowance/parents_learning_allowance.yaml @@ -9,7 +9,6 @@ parents_learning_allowance_household_income: 0 child: age: 4 - is_child: true benunits: benunit: members: ["student", "child"] @@ -32,7 +31,6 @@ parents_learning_allowance_household_income: 18_957.97 child: age: 7 - is_child: true benunits: benunit: members: ["student", "child"] @@ -55,7 +53,6 @@ parents_learning_allowance_household_income: 18_957.98 child: age: 3 - is_child: true benunits: benunit: members: ["student", "child"] @@ -78,7 +75,6 @@ current_education: TERTIARY child: age: 2 - is_child: true benunits: benunit: members: ["student", "child"] @@ -105,7 +101,6 @@ parents_learning_allowance_household_income: 10_000 child: age: 5 - is_child: true benunits: benunit: members: ["student", "child"] @@ -129,7 +124,6 @@ parents_learning_allowance_household_income: 0 child: age: 4 - is_child: true benunits: benunit: members: ["student", "child"] @@ -152,7 +146,6 @@ parents_learning_allowance_household_income: 0 child: age: 1 - is_child: true benunits: benunit: members: ["student", "child"] @@ -163,3 +156,30 @@ output: parents_learning_allowance_eligible: [false, false] parents_learning_allowance: [0, 0] + +# A dependent child has no age limit for Parents' Learning Allowance +# (Education (Student Support) Regulations 2011 regs 42 and 46): a student whose +# only dependant is an 18-year-old still at school has a dependent child. +- name: Parents' Learning Allowance counts an 18-year-old dependant at school + period: 2025 + input: + people: + student: + age: 40 + is_parent: true + parents_learning_allowance_course_eligible: true + parents_learning_allowance_household_income: 0 + teenager: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: ["student", "teenager"] + households: + household: + members: ["student", "teenager"] + country: ENGLAND + output: + parents_learning_allowance_dependent_children: 1 + parents_learning_allowance_eligible: [true, false] + parents_learning_allowance: [2_024, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml new file mode 100644 index 0000000000..fcdafae7eb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml @@ -0,0 +1,337 @@ +# UC Regulations 2013, reg 80A(2), and HB Regulations 2006, reg 75CA(2): +# annual single / family caps are 14,753 / 22,020 outside London and +# 16,967 / 25,323 in London. These cases isolate rate selection from +# exemptions and do not assert eligibility to claim UC or HB. + +- name: Single claimant aged 30 receives the outside-London single cap. + period: 2025 + input: + age: 30 + region: SOUTH_EAST + is_benefit_cap_exempt: false + output: + is_benefit_cap_single_claimant_rate: true + benefit_cap: 14_753 + +- name: Single claimant aged 30 receives the London single cap. + period: 2025 + input: + age: 30 + region: LONDON + is_benefit_cap_exempt: false + output: + is_benefit_cap_single_claimant_rate: true + benefit_cap: 16_967 + +- name: Single claimant aged 17 does not count as their own child outside London. + period: 2025 + input: + age: 17 + is_benunit_head: true + region: SOUTH_EAST + is_benefit_cap_exempt: false + output: + is_claimant_or_partner: true + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # Old age counts selected 22,020; no partner or dependant means 14,753. + # Change = 14,753 - 22,020 = -7,267 per year. + benefit_cap: 14_753 + +- name: Single claimant aged 17 does not count as their own child in London. + period: 2025 + input: + age: 17 + is_benunit_head: true + region: LONDON + is_benefit_cap_exempt: false + output: + is_benefit_cap_single_claimant_rate: true + # London change = 16,967 - 25,323 = -8,356 per year. + benefit_cap: 16_967 + +- name: A 17-year-old apprentice in the claimant's unit does not give the family cap. + period: 2025 + input: + people: + claimant: + age: 30 + is_benunit_head: true + is_parent: true + apprentice: + age: 17 + is_benunit_head: false + is_apprentice: true + current_education: POST_SECONDARY + benunits: + family: + members: [claimant, apprentice] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, apprentice] + region: SOUTH_EAST + output: + is_claimant_or_partner: [true, false] + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # UC reg 5(3) excludes education under an employment contract. + # Single cap: 14,753, down 22,020 - 14,753 = 7,267 from age-only counts. + benefit_cap: 14_753 + +- name: A 17-year-old outside education or approved training does not give the family cap. + period: 2025 + input: + people: + claimant: + age: 30 + is_benunit_head: true + is_parent: true + young_person: + age: 17 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + is_in_approved_training: false + benunits: + family: + members: [claimant, young_person] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, young_person] + region: SOUTH_EAST + output: + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # UC reg 5(1)(b): no qualifying education/training after the age-16 route. + # Change = 14,753 - 22,020 = -7,267 per year. + benefit_cap: 14_753 + +- name: A 17-year-old receiving benefits in their own right does not give the family cap. + period: 2025 + input: + people: + claimant: + age: 30 + is_benunit_head: true + is_parent: true + young_person: + age: 17 + is_benunit_head: false + current_education: POST_SECONDARY + receives_benefits_in_own_right: true + benunits: + family: + members: [claimant, young_person] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, young_person] + region: SOUTH_EAST + output: + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # UC reg 5(5) excludes own-right UC/ESA/JSA recipients from QYP status. + # Change = 14,753 - 22,020 = -7,267 per year. + benefit_cap: 14_753 + +- name: A looked-after foster child does not give the claimant the family cap. + period: 2025 + input: + people: + claimant: + age: 30 + is_benunit_head: true + child: + age: 10 + is_benunit_head: false + is_looked_after_by_local_authority: true + benunits: + family: + members: [claimant, child] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, child] + region: SOUTH_EAST + output: + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # UC reg 4(6)(a): this foster child is excluded from responsibility. + # Assume no respite or adoption placement exception under reg 4A. + # Change = 14,753 - 22,020 = -7,267 per year. + benefit_cap: 14_753 + +- name: Parent of a 17-year-old qualifying student keeps the family cap. + period: 2025 + input: + people: + parent: + age: 30 + is_benunit_head: true + is_parent: true + student: + age: 17 + is_benunit_head: false + current_education: POST_SECONDARY + benunits: + family: + members: [parent, student] + is_benefit_cap_exempt: false + households: + home: + members: [parent, student] + region: SOUTH_EAST + output: + is_couple: false + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true + is_benefit_cap_single_claimant_rate: false + # Responsibility under UC regs 4-5 gives the 22,020 annual family limit. + benefit_cap: 22_020 + +- name: Lone parent of an 18-year-old qualifying student keeps the London family cap. + period: 2025 + input: + people: + parent: + age: 30 + is_benunit_head: true + is_parent: true + student: + age: 18 + is_benunit_head: false + current_education: POST_SECONDARY + benunits: + family: + members: [parent, student] + is_benefit_cap_exempt: false + households: + home: + members: [parent, student] + region: LONDON + output: + is_claimant_or_partner: [true, false] + is_couple: false + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true + is_benefit_cap_single_claimant_rate: false + # Same 25,323 as before, now because of responsibility rather than two 18+ members. + benefit_cap: 25_323 + +- name: Couple aged 30 and 17 receives the family cap without counting the partner as a child. + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + current_education: POST_SECONDARY + benunits: + family: + members: [claimant, partner] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, partner] + region: SOUTH_EAST + output: + is_couple: true + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: false + # Couple composition selects 22,020. This is not a UC eligibility test: + # UC reg 3(3) permits some couples to claim as singles, not modelled here. + benefit_cap: 22_020 + +- name: Explicitly identified 19-year-old non-dependant does not imply a partner. + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + non_dependant: + age: 19 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + family: + members: [claimant, non_dependant] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, non_dependant] + region: SOUTH_EAST + output: + is_couple: false + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_benefit_cap_single_claimant_rate: true + # Old two-adult count gave 22,020. With explicit roles, no partner or QYP: + # change = 14,753 - 22,020 = -7,267 per year. + benefit_cap: 14_753 + +- name: Benefit-cap exemption still overrides the selected single rate. + period: 2025 + input: + age: 30 + region: SOUTH_EAST + is_benefit_cap_exempt: true + universal_credit_pre_benefit_cap: 100_000 + output: + is_benefit_cap_single_claimant_rate: true + # Exemption makes the cap infinite: max(100,000 - infinity, 0) = 0. + # Assert the resulting reduction because the YAML runner cannot compare infinities. + benefit_cap_reduction: 0 + +- name: A Housing Benefit young person keeps the family cap though not a UC qualifying young person + # The 19-year-old started non-advanced education at 17 and is past UC's + # terminal date, so is not a UC qualifying young person (UC reg 5), but is a + # Child Benefit qualifying young person, which Housing Benefit uses (HB reg + # 19). One cap serves both, so the family rate applies: 22,020. + period: 2025 + input: + people: + parent: + age: 40 + is_parent: true + student: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + benunits: + benunit: + members: [parent, student] + is_benefit_cap_exempt: false + households: + household: + members: [parent, student] + region: SOUTH_EAST + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_child_benefit: [false, true] + benefit_cap: 22_020 + +- name: A 16-year-old outside education keeps the family cap (UC reg 5(1)(a) window) + # Every 16-year-old is a UC qualifying young person until the 1 September + # after their 16th birthday; annual ages cannot place that date. + period: 2025 + input: + people: + parent: + age: 40 + is_parent: true + teenager: + age: 16 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, teenager] + is_benefit_cap_exempt: false + households: + household: + members: [parent, teenager] + region: SOUTH_EAST + output: + benefit_cap: 22_020 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml new file mode 100644 index 0000000000..8c05d0d82f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml @@ -0,0 +1,162 @@ +# Means tests count the income of the claimant and any partner and, as the +# model did before, of the programme's own children or young persons. A member +# who is none of these (for example an 18- or 19-year-old presumed to be the +# claimant's child but not in education) does not count. + +- name: An earning 18-year-old who is neither partner nor dependant does not reduce the parent's UC + # Default inputs: the 18-year-old is presumed the parent's child, is not in + # education (so not a UC qualifying young person) and is not a claimant. + # UC is the same as for the parent and the 10-year-old alone (next case). + period: 2026 + absolute_error_margin: 1 + input: + people: + parent: + age: 40 + young_adult: + age: 18 + employment_income: 15_000 + child: + age: 10 + benunits: + benunit: + members: [parent, young_adult, child] + households: + household: + members: [parent, young_adult, child] + output: + is_claimant_or_partner: [true, false, false] + uc_earned_income: 0 + universal_credit: 9_321.36 + +- name: The same parent and child without the 18-year-old + period: 2026 + absolute_error_margin: 1 + input: + people: + parent: + age: 40 + child: + age: 10 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + universal_credit: 9_321.36 + +- name: A 17-year-old outside education is not a UC young person, so their pay does not count + period: 2026 + absolute_error_margin: 1 + input: + people: + parent: + age: 40 + teenager: + age: 17 + current_education: NOT_IN_EDUCATION + employment_income: 5_000 + child: + age: 10 + benunits: + benunit: + members: [parent, teenager, child] + households: + household: + members: [parent, teenager, child] + output: + is_child_or_qualifying_young_person_for_universal_credit: [false, false, true] + is_claimant_or_partner: [true, false, false] + uc_earned_income: 0 + +- name: A non-dependant 19-year-old's pay is not the claimant's income under any legacy means test + # The claimant earns 10,000 and a 19-year-old presumed their child (not in + # education, so not a young person) earns 15,000. Every applicable income + # equals the claimant-alone case below. + period: 2024 + absolute_error_margin: 1 + input: + people: + claimant: + age: 40 + employment_income: 10_000 + young_adult: + age: 19 + employment_income: 15_000 + benunits: + benunit: + members: [claimant, young_adult] + households: + household: + members: [claimant, young_adult] + output: + is_claimant_or_partner: [true, false] + housing_benefit_applicable_income: 9_655.42 + income_support_applicable_income: 9_740 + tax_credits_applicable_income: 10_000 + council_tax_reduction_applicable_income: 10_000 + uc_earned_income: 10_000 + +- name: The same claimant alone + period: 2024 + absolute_error_margin: 1 + input: + people: + claimant: + age: 40 + employment_income: 10_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + housing_benefit_applicable_income: 9_655.42 + income_support_applicable_income: 9_740 + tax_credits_applicable_income: 10_000 + council_tax_reduction_applicable_income: 10_000 + uc_earned_income: 10_000 + +- name: A non-member's hours do not earn the claimant the HB worker disregard + # HB Sch 4 para 17 tests the claimant's or partner's work. The 19-year-old + # (not in education, presumed the claimant's child, not a young person) + # works 35 hours; only the single-claimant disregard applies, as for the + # claimant alone. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + young_adult: + age: 19 + weekly_hours: 35 + employment_income: 15_000 + benunits: + benunit: + members: [claimant, young_adult] + households: + household: + members: [claimant, young_adult] + output: + is_claimant_or_partner: [true, false] + housing_benefit_applicable_income_disregard: 344.58 + +- name: The same claimant alone gets the same HB disregard + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + housing_benefit_applicable_income_disregard: 344.58 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml new file mode 100644 index 0000000000..dbf4bb5dbf --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml @@ -0,0 +1,216 @@ +- name: An explicit Pension Credit earnings input is the claimant and partner's earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + young_person: + age: 18 + is_in_non_advanced_education: true + employment_income: 3_000 + benunits: + benunit: + members: [claimant, young_person] + pension_credit_earnings: 10_000 + output: + # pension_credit_earnings counts only the claimant and partner (SPCA s.5), + # so an input of 10,000 is taken as theirs; the dependant's 3,000 is not + # added. + pension_credit_income: 10_000 + +- name: Pension Credit earnings exclude a qualifying young person's earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + employment_income: 5_000 + young_person: + age: 18 + is_in_non_advanced_education: true + employment_income: 3_000 + benunits: + benunit: + members: [claimant, young_person] + output: + # Only the claimant's 5,000 counts. + pension_credit_earnings: 5_000 + +- name: Pension Credit excludes an 18-year-old qualifying young person's income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + state_pension: 8_000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + young_person: + age: 18 + is_in_non_advanced_education: true + employment_income: 3_000 + self_employment_income: 1_000 + property_income: 500 + benunits: + benunit: + members: [claimant, young_person] + working_tax_credit: 200 + pension_credit_deemed_income: 104 + output: + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_pension_credit: [false, true] + # SPCA 2002 s.5: 8,000 claimant pension + 200 unit tax credit + 104 + # unit tariff income = 8,304; the dependant's 4,500 income is excluded. + pension_credit_income: 8_304 + +- name: Pension Credit excludes a 19-year-old qualifying young person's deductions + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + state_pension: 10_000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + employment_income: 3_000 + income_tax: 100 + national_insurance: 100 + pension_contributions: 2_000 + benunits: + benunit: + members: [claimant, young_person] + output: + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_pension_credit: [false, true] + # SPCA 2002 s.5: only the claimant's 10,000 pension counts. Neither + # the dependant's 3,000 earnings nor 100 + 100 + 50% * 2,000 deductions count. + pension_credit_income: 10_000 + +- name: Pension Credit means-tested basic income is restricted to the claimant and partner + period: 2026 + absolute_error_margin: 0.01 + input: + gov.contrib.ubi_center.basic_income.interactions.include_in_means_tests: true + people: + claimant: + age: 75 + is_parent: true + state_pension: 8_000 + basic_income: 100 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + young_person: + age: 18 + is_in_non_advanced_education: true + basic_income: 1_000 + benunits: + benunit: + members: [claimant, young_person] + output: + # The reform elects to count basic income. SPCA 2002 s.5 limits whose + # income is counted: 8,000 pension + 100 claimant basic income = 8,100. + pension_credit_income: 8_100 + +- name: Pension Credit counts both partners' income deductions and carer additions + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + state_pension: 5_000 + employment_income: 1_000 + jsa_contrib: 100 + income_tax: 100 + national_insurance: 0 + pension_contributions: 200 + is_carer_for_benefits: true + partner: + age: 75 + private_pension_income: 2_000 + self_employment_income: 500 + esa_contrib: 200 + income_tax: 50 + national_insurance: 25 + pension_contributions: 100 + is_carer_for_benefits: true + benunits: + benunit: + members: [claimant, partner] + pension_credit_deemed_income: 104 + output: + is_claimant_or_partner: [true, true] + # SPCA 2002 s.5: gross = 5,000 + 1,000 + 100 + 2,000 + 500 + 200 + # + 104 = 8,904; deductions = 100 + 50 + 25 + 50% * 300 = 325. + pension_credit_income: 8_579 + # Reg.9: remove the claimant's 100 JSA and partner's 200 ESA. + savings_credit_income: 8_279 + # Sch.I para.4 and the 2026 carer addition: 2 * 48.15 * 52 = 5,007.60. + carer_minimum_guarantee_addition: 5_007.60 + +- name: A qualifying young person who is a carer does not receive a Pension Credit carer addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + is_carer_for_benefits: false + young_person: + age: 18 + is_in_non_advanced_education: true + is_carer_for_benefits: true + benunits: + benunit: + members: [claimant, young_person] + output: + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_pension_credit: [false, true] + # Sch.I para.4 counts entitled claimants/partners: 0 * 48.15 * 52 = 0. + carer_minimum_guarantee_addition: 0 + +- name: Savings Credit does not subtract a dependant's excluded benefits from claimant income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + state_pension: 10_000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + young_person: + age: 19 + is_in_non_advanced_education: true + esa_contrib: 200 + receives_benefits_in_own_right: true + benunits: + benunit: + members: [claimant, young_person] + output: + is_claimant_or_partner: [true, false] + # SPCA 2002 s.5 and reg.9: 10,000 claimant pension - 0 claimant/partner + # excluded benefits = 10,000. The dependant's 200 ESA never enters income. + pension_credit_income: 10_000 + savings_credit_income: 10_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_rates.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_rates.yaml new file mode 100644 index 0000000000..257efed7e3 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_rates.yaml @@ -0,0 +1,90 @@ +# SPCA 2002 ss.2(5), 3 and SPC Regulations 2002 regs 6-7, Sch IIA para 10. +# These integration cases use the foundation's relation_type formula. +- name: Single pensioner with a qualifying young person receives single rates and Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + is_parent: true + private_pension_income: 12_000 + young_person: + age: 18 + is_in_non_advanced_education: true + benunits: + benunit: + members: [claimant, young_person] + output: + relation_type: SINGLE + is_pension_credit_eligible: true + is_savings_credit_eligible: true + # 238 * 52 = 12,376; eldest QYP born before 2017: 81.07 * 52 = 4,215.64. + standard_minimum_guarantee: 12_376 + child_minimum_guarantee_addition: 4_215.64 + minimum_guarantee: 16_591.64 + pension_credit_income: 12_000 + # Guarantee: 16,591.64 - 12,000 = 4,591.64. + guarantee_credit: 4_591.64 + # Savings: (12,000 - 208.07 * 52) * 0.6 = 708.216, below maximum 933.816. + savings_credit: 708.216 + pension_credit_entitlement: 5_299.856 + +- name: Pensioner couple with a qualifying young person retains couple rates and Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + is_parent: true + private_pension_income: 10_000 + partner: + age: 79 + private_pension_income: 8_000 + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + benunits: + benunit: + members: [claimant, partner, young_person] + output: + relation_type: COUPLE + is_pension_credit_eligible: true + is_savings_credit_eligible: true + # 363.25 * 52 = 18,889; child addition 81.07 * 52 = 4,215.64. + standard_minimum_guarantee: 18_889 + minimum_guarantee: 23_104.64 + pension_credit_income: 18_000 + guarantee_credit: 5_104.64 + # (18,000 - 329.75 * 52) * 0.6 = 511.80, below maximum 1,045.20. + savings_credit: 511.80 + pension_credit_entitlement: 5_616.44 + +- name: Guarantee Credit is zero for a working-age family + # Guarantee credit is part of State Pension Credit (SPCA 2002 ss.1-2), so a + # working-age family on a low income has none. Previously it was computed for + # everyone below the minimum guarantee, which passported working-age families + # into the targeted (two-year-old) childcare entitlement. + period: 2026 + input: + people: + parent_1: + age: 30 + is_parent: true + parent_2: + age: 30 + is_parent: true + toddler: + age: 2 + benunits: + benunit: + members: [parent_1, parent_2, toddler] + households: + household: + members: [parent_1, parent_2, toddler] + country: ENGLAND + output: + is_pension_credit_eligible: false + guarantee_credit: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/carer_and_severe_disability_additions.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/carer_and_severe_disability_additions.yaml index 577d4c653b..4c8758bcbb 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/carer_and_severe_disability_additions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/carer_and_severe_disability_additions.yaml @@ -69,6 +69,7 @@ attendance_allowance: 100 partner: age: 70 + attendance_allowance: 100 care_hours: 35 carers_allowance_reported: 0 would_claim_carers_allowance: false @@ -82,9 +83,80 @@ output: receives_carer_benefit: [false, false] is_carer_for_benefits: [false, true] + # Both partners qualify (Sch I para 1(1)(b)) and no carer benefit is paid, + # so the double amount applies (reg 6(5)): 2 x 67.30 x 52. + severe_disability_minimum_guarantee_addition: 2 * 67.3 * 52 + +- name: Severe disability addition needs both partners to qualify + # Sch I para 1(1)(b)-(c): a claimant with a partner is severely disabled + # only if both receive a qualifying benefit, or one does and the other is + # blind. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + partner: + age: 68 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + severe_disability_minimum_guarantee_addition: 0 + +- name: Severe disability addition with one qualifying partner and a blind partner + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + partner: + age: 68 + is_blind: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + # Sch I para 1(1)(c): single amount, 67.30 x 52. severe_disability_minimum_guarantee_addition: 67.3 * 52 -- name: Severe disability addition is withheld when the partner receives Carer's Allowance +- name: A child does not rescue a couple where only one partner qualifies + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + partner: + age: 68 + child: + age: 10 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + country: ENGLAND + output: + severe_disability_minimum_guarantee_addition: 0 + +- name: Severe disability addition is withheld when Carer's Allowance is paid for the qualifying partner of a blind partner + # Sch I para 1(1)(c)(iv): no carer benefit may be paid for the partner who + # receives the qualifying benefit. The carer is assumed to care for them. period: 2021 absolute_error_margin: 0.01 input: @@ -94,6 +166,7 @@ attendance_allowance: 100 partner: age: 70 + is_blind: true care_hours: 35 carers_allowance_reported: 1 benunits: @@ -106,3 +179,133 @@ output: receives_carer_benefit: [false, true] severe_disability_minimum_guarantee_addition: 0 + +- name: Both partners qualify and Carer's Allowance is paid for one of them + # Sch I para 1(1)(b) still holds when a carer benefit is paid for one only + # of the partners; reg 6(5)(a) then gives the single amount, 67.30 x 52. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + partner: + age: 70 + attendance_allowance: 100 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + receives_carer_benefit: [false, true] + severe_disability_minimum_guarantee_addition: 67.3 * 52 + +- name: Both partners qualify and carer benefits are paid for both + # Carer benefits for both partners fail Sch I para 1(1)(b), and (c) needs a + # blind partner, so no addition. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + care_hours: 35 + carers_allowance_reported: 1 + partner: + age: 70 + attendance_allowance: 100 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + receives_carer_benefit: [true, true] + severe_disability_minimum_guarantee_addition: 0 + +- name: A single claimant with a carer benefit paid for them gets no addition + # Sch I para 1(1)(a)(iii). Supplying the claimant flag replaces the formula + # for everyone, so set it for both. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + is_claimant_or_partner: true + carer: + age: 45 + care_hours: 35 + carers_allowance_reported: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, carer] + households: + household: + members: [claimant, carer] + country: ENGLAND + output: + severe_disability_minimum_guarantee_addition: 0 + +- name: A single claimant's own Carer's Allowance does not bar the addition + # Sch I para 1(1)(a)(iii) bars an allowance paid "in respect of caring for + # him"; the claimant's own award is for caring for someone else. 67.30 x 52. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + receives_carer_benefit: true + severe_disability_minimum_guarantee_addition: 67.3 * 52 + +- name: The qualifying partner's own Carer's Allowance does not bar head (c) + # The partner receiving attendance allowance cares for the blind partner; + # (c)(iv) bars only a carer benefit paid for caring for the qualifying + # partner. Single amount, 67.30 x 52. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + attendance_allowance: 100 + care_hours: 35 + carers_allowance_reported: 1 + partner: + age: 70 + is_blind: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + receives_carer_benefit: [true, false] + severe_disability_minimum_guarantee_addition: 67.3 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml index e51465b6e8..a99d4cbda2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_minimum_guarantee_addition.yaml @@ -17,3 +17,27 @@ output: child_minimum_guarantee_addition: 7_854.6 minimum_guarantee: 20_230.6 + +- name: An eldest qualifying young person receives the higher child amount ahead of a younger child. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + is_parent: true + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + child: + age: 6 + birth_year: 2020 + benunits: + benunit: + members: [claimant, young_person, child] + output: + # Sch IIA para 10: eldest QYP gets 81.07, younger child 69.98 per week. + # (81.07 + 69.98) * 52 = 7,854.60; single guarantee = 238 * 52. + child_minimum_guarantee_addition: 7_854.60 + minimum_guarantee: 20_230.60 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_claimant_partner.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_claimant_partner.yaml new file mode 100644 index 0000000000..b289708dd7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_claimant_partner.yaml @@ -0,0 +1,82 @@ +# SPC Regulations 2002 Sch I paras 1-2 and reg 6(5). +# Children and qualifying young people do not block the addition or receive +# a claimant/partner addition. The 2026 weekly single amount is GBP 86.05. +- name: A child without disability benefits does not block the claimant's addition. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 79 + attendance_allowance: 100 + child: + age: 10 + benunits: + benunit: + members: [claimant, child] + output: + # One qualifying claimant: 1 * 86.05 * 52 = 4,474.60. + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: An 18-year-old qualifying young person's PIP does not add a second claimant amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 79 + is_parent: true + attendance_allowance: 100 + young_person: + age: 18 + is_in_non_advanced_education: true + pip_dl: 100 + benunits: + benunit: + members: [claimant, young_person] + output: + # Only the claimant qualifies: 1 * 86.05 * 52 = 4,474.60. + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: Two qualifying partners with a disabled young person receive two claimant amounts. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 79 + is_parent: true + attendance_allowance: 100 + partner: + age: 70 + attendance_allowance: 100 + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + pip_dl: 100 + benunits: + benunit: + members: [claimant, partner, young_person] + output: + # Reg 6(5)(b): 2 * 86.05 * 52 = 8,949.20, excluding the dependant. + severe_disability_minimum_guarantee_addition: 8_949.20 + +- name: A dependant's disability benefit alone does not qualify the claimant. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 79 + is_parent: true + young_person: + age: 18 + is_in_non_advanced_education: true + pip_dl: 100 + benunits: + benunit: + members: [claimant, young_person] + output: + # No qualifying claimant: 0 * 86.05 * 52 = 0. + severe_disability_minimum_guarantee_addition: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml index d2fa9efdae..00a7b4b387 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml @@ -10,6 +10,129 @@ output: is_pension_credit_eligible: false +- name: Pensioner responsible for an 18-year-old student has one pension-age claimant. + period: 2026 + input: + people: + claimant: + age: 79 + is_parent: true + young_person: + age: 18 + is_in_non_advanced_education: true + benunits: + benunit: + members: [claimant, young_person] + output: + # SPCA ss.1(2)(b), 4(1A): 1 of 1 claimants is pension age. + is_claimant_or_partner: [true, false] + is_pension_credit_eligible: true + +- name: Pensioner responsible for a 19-year-old trainee has one pension-age claimant. + period: 2026 + input: + people: + claimant: + age: 79 + is_parent: true + young_person: + age: 19 + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 18 + benunits: + benunit: + members: [claimant, young_person] + output: + # SPCA ss.1(2)(b), 4(1A): 1 of 1 claimants is pension age. + is_claimant_or_partner: [true, false] + is_pension_credit_eligible: true + +- name: Pension-age couple with a 19-year-old student is eligible. + period: 2026 + input: + people: + claimant: + age: 79 + is_parent: true + partner: + age: 70 + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + benunits: + benunit: + members: [claimant, partner, young_person] + output: + # SPCA ss.1(2)(b), 4(1A): 2 of 2 claimants are pension age. + is_claimant_or_partner: [true, true, false] + is_pension_credit_eligible: true + +- name: A 19-year-old student partner is still a mixed-age couple. + period: 2026 + input: + people: + claimant: + age: 79 + is_claimant_or_partner: true + partner: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + output: + # An identified partner: 1 of 2 claimants is pension age, so s.4(1A) bars PC. + is_pension_credit_eligible: false + +- name: Without relationships, a 19-year-old living with a 79-year-old is presumed their grandchild. + period: 2026 + input: + people: + claimant: + age: 79 + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 18 + benunits: + benunit: + members: [claimant, young_person] + output: + # Under 20 and 60 years younger than the head: presumed a child of the + # household, not a partner (PolicyEngine presumption), so the pensioner + # claims alone. + is_claimant_or_partner: [true, false] + is_pension_credit_eligible: true + +- name: An explicitly identified under-18 partner must meet Pension Credit age conditions. + period: 2026 + input: + people: + claimant: + age: 79 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + output: + # SPCA s.4(1A): 1 of 2 claimants is pension age; age-18 counting missed this partner. + is_pension_credit_eligible: false + +- name: Pension Credit needs a claimant even when every member is pension age. + period: 2026 + input: + age: 79 + is_claimant_or_partner: false + output: + # SPCA s.1: no claimant means no entitlement, even though 0 of 0 meet SPA. + is_pension_credit_eligible: false + - name: Case 2, single adult above State Pension age is eligible. period: 2026 input: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml index 42ee1cbd29..8e32aebaa7 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml @@ -24,7 +24,6 @@ benunits: benunit: members: [person] - relation_type: SINGLE savings_credit_income: 10200 output: is_savings_credit_eligible: true @@ -40,7 +39,6 @@ benunits: benunit: members: [person] - relation_type: SINGLE savings_credit_income: 10_000 output: is_savings_credit_eligible: false @@ -60,7 +58,6 @@ benunits: benunit: members: [person, spouse] - relation_type: COUPLE savings_credit_income: 16000 output: is_savings_credit_eligible: true @@ -80,7 +77,25 @@ benunits: benunit: members: [person, spouse] - relation_type: COUPLE savings_credit_income: 16000 output: is_savings_credit_eligible: false + +- name: A nonclaimant's age-requirement flag cannot establish Savings Credit eligibility. + period: 2026 + input: + people: + claimant: + age: 68 + young_person: + age: 19 + is_claimant_or_partner: false + # Deliberate override to isolate s.3(1)'s claimant/partner restriction. + meets_savings_credit_age_requirement: true + benunits: + benunit: + members: [claimant, young_person] + savings_credit_income: 12_000 + output: + # 12,000 > 208.07 * 52, but no claimant reached SPA before the cutoff. + is_savings_credit_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml index 521f62d955..673614ccf2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml @@ -62,3 +62,75 @@ output: marriage_allowance: [0, 0, 0, 0] income_tax: [9_432, 4_486, 0, 0] + +# Marriage Allowance is an election between spouses or civil partners (ITA 2007 +# s.55B, s.55C(1)(a)): the claimant and partner of a married benefit unit, never +# a child in it, whatever the child's age. +- name: An 18-year-old dependant's unused allowance does not transfer to a parent + # p1 is a basic-rate taxpayer with no unused allowance of their own spouse + # (p2 earns 60,000), so p1 gets no Marriage Allowance. Previously the + # 18-year-old's unused 12,570 counted as a "partner's" allowance and p1 got + # 1,260 (a 252 tax cut). + period: 2025 + input: + people: + p1: + age: 45 + is_parent: true + employment_income: 30_000 + p2: + age: 45 + is_parent: true + employment_income: 60_000 + student: + age: 18 + current_education: UPPER_SECONDARY + benunit: + members: [p1, p2, student] + is_married: true + output: + partners_unused_personal_allowance: [0, 0, 0] + marriage_allowance: [0, 0, 0] + marital_status: [MARRIED, MARRIED, SINGLE] + +- name: A lone parent with a 19-year-old at school is not married + # With an identified parent, the 19-year-old in non-advanced education is a + # dependant, so the benefit unit has no couple and no Marriage Allowance. + # Previously two members aged 18+ made the unit "married" by default and the + # parent got 1,260 transferred from the 19-year-old. + period: 2025 + input: + people: + parent: + age: 45 + is_parent: true + employment_income: 40_000 + student: + age: 19 + current_education: UPPER_SECONDARY + employment_income: 1_000 + benunit: + members: [parent, student] + output: + is_married: false + marriage_allowance: [0, 0] + +- name: Parents do not receive Marriage Allowance from an 18-year-old's allowance + # Default inputs (no relationship or education flags). The couple are the + # spouses; their 18-year-old is neither. Each parent earns 40,000, so both + # use their full personal allowance and neither has one to transfer. + period: 2025 + input: + people: + parent_1: + age: 42 + employment_income: 40_000 + parent_2: + age: 40 + employment_income: 40_000 + student: + age: 18 + benunit: + members: [parent_1, parent_2, student] + output: + marriage_allowance: [0, 0, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/tax_free_childcare/tax_free_childcare_work_condition.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/tax_free_childcare/tax_free_childcare_work_condition.yaml index 684d8a48b0..b0f8195c60 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/tax_free_childcare/tax_free_childcare_work_condition.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/tax_free_childcare/tax_free_childcare_work_condition.yaml @@ -3,11 +3,9 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 0 spouse: - is_adult: true in_work: true incapacity_benefit: 0 benunits: @@ -21,12 +19,10 @@ input: people: person: - is_adult: true is_parent: true in_work: false incapacity_benefit: 0 spouse: - is_adult: true in_work: false is_parent: true incapacity_benefit: 0 @@ -41,11 +37,9 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 0 spouse: - is_adult: true in_work: false incapacity_benefit: 100 benunits: @@ -59,11 +53,9 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 0 spouse: - is_adult: true in_work: false incapacity_benefit: 100 benunits: @@ -77,12 +69,10 @@ input: people: person: - is_adult: true in_work: true is_parent: true incapacity_benefit: 0 spouse: - is_adult: true in_work: false is_parent: true incapacity_benefit: 0 @@ -97,7 +87,6 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 0 benunits: @@ -111,7 +100,6 @@ input: people: person: - is_adult: true in_work: false incapacity_benefit: 0 benunits: @@ -125,11 +113,9 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 100 spouse: - is_adult: true in_work: false incapacity_benefit: 100 benunits: @@ -143,7 +129,6 @@ input: people: person: - is_adult: true in_work: true incapacity_benefit: 100 benunits: @@ -157,7 +142,6 @@ input: people: person: - is_adult: true in_work: false incapacity_benefit: 100 benunits: @@ -171,7 +155,6 @@ input: people: person: - is_adult: true in_work: false tax_free_childcare_on_qualifying_leave: true benunits: @@ -185,11 +168,9 @@ input: people: person: - is_adult: true is_parent: true in_work: true spouse: - is_adult: true is_parent: true in_work: false tax_free_childcare_on_qualifying_leave: true @@ -204,11 +185,9 @@ input: people: person: - is_adult: true is_parent: true in_work: true spouse: - is_adult: true is_parent: true in_work: false statutory_maternity_pay: 100 @@ -223,11 +202,9 @@ input: people: person: - is_adult: true is_parent: true in_work: true spouse: - is_adult: true is_parent: true in_work: false tax_free_childcare_on_adoption_leave: true @@ -242,11 +219,9 @@ input: people: person: - is_adult: true is_parent: true in_work: true spouse: - is_adult: true is_parent: true in_work: false tax_free_childcare_on_shared_parental_leave: true @@ -255,3 +230,113 @@ members: [person, spouse] output: tax_free_childcare_work_condition: true + +# The work condition applies to the person applying and their partner +# (Childcare Payments Act 2014 s.3(1), s.6, s.9; SI 2015/448 regs 3 and 9): +# the claimant or partner of the benefit unit, aged 16 or over. A child's own +# work does not count, and a benefit unit with no one aged 18 or over is not a +# couple. + +- name: A working 17-year-old child does not meet a lone parent's work condition + period: 2025 + input: + people: + parent: + age: 40 + is_parent: true + in_work: false + incapacity_benefit: 0 + teenager: + age: 17 + in_work: true + incapacity_benefit: 0 + benunits: + benunit: + members: [parent, teenager] + output: + tax_free_childcare_work_condition: [false, false] + +- name: A non-working 17-year-old lone parent is a single applicant, not a couple + period: 2025 + input: + people: + parent: + age: 17 + in_work: false + incapacity_benefit: 0 + baby: + age: 0 + in_work: false + incapacity_benefit: 0 + benunits: + benunit: + members: [parent, baby] + output: + tax_free_childcare_work_condition: [false, false] + +- name: A working 17-year-old lone parent meets the work condition + period: 2025 + input: + people: + parent: + age: 17 + is_parent: true + in_work: true + incapacity_benefit: 0 + baby: + age: 0 + in_work: false + incapacity_benefit: 0 + benunits: + benunit: + members: [parent, baby] + output: + tax_free_childcare_work_condition: [true, false] + +- name: A couple where only one works is not rescued by an 18-year-old dependant + period: 2025 + input: + people: + parent_1: + age: 40 + is_parent: true + in_work: true + incapacity_benefit: 0 + parent_2: + age: 40 + is_parent: true + in_work: false + incapacity_benefit: 0 + student: + age: 18 + current_education: UPPER_SECONDARY + in_work: true + incapacity_benefit: 0 + benunits: + benunit: + members: [parent_1, parent_2, student] + output: + tax_free_childcare_work_condition: [false, false, false] + +- name: A working lone parent with an 18-year-old and a toddler meets the work condition + # Default inputs: the 18-year-old is the parent's child, not a partner who + # would also have to work. + period: 2025 + input: + people: + parent: + age: 40 + employment_income: 80_000 + in_work: true + incapacity_benefit: 0 + student: + age: 18 + incapacity_benefit: 0 + toddler: + age: 3 + incapacity_benefit: 0 + benunits: + benunit: + members: [parent, student, toddler] + output: + tax_free_childcare_work_condition: [true, false, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml index ce21f7d8a4..6e52f66d3d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -239,9 +239,10 @@ council_tax: 2_000 savings: 0 output: - # The son's guarantee_credit is calculated as 7,376, but he is not - # eligible for Pension Credit, so he is not in receipt of it. - guarantee_credit: [3_376, 7_376] + # The son is not eligible for Pension Credit, so he has no guarantee + # credit (guarantee_credit is defined only for Pension Credit-eligible + # benefit units) and is not in receipt of it. + guarantee_credit: [3_376, 0] in_receipt_of_guarantee_credit: [true, false] council_tax_reduction_applicable_income: [0, 5_000] # The national schemes' non-dependant deduction currently uses Housing @@ -277,7 +278,8 @@ council_tax: 2_000 savings: 20_000 output: - guarantee_credit: 8_849 + # Not eligible for Pension Credit, so no guarantee credit. + guarantee_credit: 0 is_pension_credit_eligible: false in_receipt_of_guarantee_credit: false council_tax_reduction: 0 @@ -332,7 +334,7 @@ council_tax_reduction_applicable_income: 0 council_tax_reduction: 2_000 -- name: Wales working-age earner with a calculated guarantee credit keeps the taper +- name: Wales working-age earner, not eligible for Pension Credit, keeps the taper period: 2026 absolute_error_margin: 0.01 input: @@ -354,7 +356,8 @@ council_tax: 2_000 savings: 0 output: - guarantee_credit: 2_376 + # Working age, so not eligible for Pension Credit: no guarantee credit. + guarantee_credit: 0 in_receipt_of_guarantee_credit: false council_tax_reduction_applicable_income: 10_000 council_tax_reduction: 993.72 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_claimant_non_dep_exemption.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_claimant_non_dep_exemption.yaml new file mode 100644 index 0000000000..ec6636d1fa --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_claimant_non_dep_exemption.yaml @@ -0,0 +1,103 @@ +- name: Dependent young person PIP does not exempt the claimant household + period: 2025 + input: + people: + claimant: + age: 70 + is_parent: true + other_member: + age: 18 + is_in_non_advanced_education: true + pip_dl: 100 + non_dependant: + age: 25 + benunits: + claimant_unit: + members: [claimant, other_member] + other_unit: + members: [non_dependant] + households: + household: + members: [claimant, other_member, non_dependant] + output: + # CTR SI 2012/2885 Sch 1 para 8(6): only applicant/partner benefits count. + council_tax_reduction_household_has_non_dep_exemption: false + +- name: Under-18 partner PIP exempts the claimant household + period: 2025 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + is_parent: true + other_member: + age: 17 + is_in_non_advanced_education: true + is_claimant_or_partner: true + pip_dl: 100 + non_dependant: + age: 25 + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant, other_member] + other_unit: + members: [non_dependant] + households: + household: + members: [claimant, other_member, non_dependant] + output: + # CTR SI 2012/2885 Sch 1 para 8(6): only applicant/partner benefits count. + is_couple: [true, false] + council_tax_reduction_household_has_non_dep_exemption: true + +- name: Claimant PIP continues to exempt the household + period: 2025 + input: + people: + claimant: + age: 70 + is_parent: true + pip_dl: 100 + other_member: + age: 18 + is_in_non_advanced_education: true + non_dependant: + age: 25 + benunits: + claimant_unit: + members: [claimant, other_member] + other_unit: + members: [non_dependant] + households: + household: + members: [claimant, other_member, non_dependant] + output: + # CTR SI 2012/2885 Sch 1 para 8(6): only applicant/partner benefits count. + council_tax_reduction_household_has_non_dep_exemption: true + +- name: Dependent young person blindness does not exempt the claimant household + period: 2025 + input: + people: + claimant: + age: 70 + is_parent: true + other_member: + age: 18 + is_in_non_advanced_education: true + is_blind: true + non_dependant: + age: 25 + benunits: + claimant_unit: + members: [claimant, other_member] + other_unit: + members: [non_dependant] + households: + household: + members: [claimant, other_member, non_dependant] + output: + # CTR SI 2012/2885 Sch 1 para 8(6): only applicant/partner benefits count. + council_tax_reduction_household_has_non_dep_exemption: false diff --git a/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/two_child_limit_payment.yaml b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/two_child_limit_payment.yaml index 4591c64aca..205ccf6291 100644 --- a/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/two_child_limit_payment.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/social_security_scotland/two_child_limit_payment.yaml @@ -8,21 +8,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -42,21 +35,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -76,26 +62,17 @@ people: parent: age: 30 - is_child: false child_1: age: 8 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 6 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 4 - is_child: true - child_index: 3 uc_is_child_limit_affected: true child_4: age: 2 - is_child: true - child_index: 4 uc_is_child_limit_affected: true benunits: benunit: @@ -115,31 +92,20 @@ people: parent: age: 35 - is_child: false child_1: age: 12 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 10 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 7 - is_child: true - child_index: 3 uc_is_child_limit_affected: true child_4: age: 5 - is_child: true - child_index: 4 uc_is_child_limit_affected: true child_5: age: 3 - is_child: true - child_index: 5 uc_is_child_limit_affected: true benunits: benunit: @@ -159,16 +125,11 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false benunits: benunit: @@ -187,21 +148,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -220,21 +174,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -253,21 +200,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -286,24 +226,16 @@ people: parent_1: age: 35 - is_child: false parent_2: age: 33 - is_child: false child_1: age: 8 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 5 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 2 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: @@ -322,21 +254,14 @@ people: parent: age: 30 - is_child: false child_1: age: 5 - is_child: true - child_index: 1 uc_is_child_limit_affected: false child_2: age: 3 - is_child: true - child_index: 2 uc_is_child_limit_affected: false child_3: age: 1 - is_child: true - child_index: 3 uc_is_child_limit_affected: true benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml new file mode 100644 index 0000000000..ce6962b0a0 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml @@ -0,0 +1,373 @@ +# Benefit-unit structure: the claimant and partner, couples, and the HBAI +# population groups. +# +# HBAI FYE 2025 glossary: a dependent child is under 16, or 16-19 and not +# partnered, living with parents or a responsible adult, and in full-time +# non-advanced education or unwaged government training. Everyone else is an +# adult; pensioners are adults at or above State Pension age (66 in 2025). +# Benefit units are a single adult or a couple plus dependent children, so the +# claimant and partner are the benefit unit's HBAI adults. + +- name: Lone parent with an 18-year-old at school is a lone parent, not a couple + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + child: + age: 10 + benunits: + benunit: + members: [parent, student, child] + households: + household: + members: [parent, student, child] + output: + is_hbai_dependent_child: [false, true, true] + is_claimant_or_partner: [true, false, false] + hbai_person_type: [WORKING_AGE_ADULT, CHILD, CHILD] + relation_type: SINGLE + is_couple: false + is_lone_parent: true + is_single_person: false + is_married: false + marital_status: [SINGLE, SINGLE, SINGLE] + eldest_claimant_or_partner_age: 40 + youngest_claimant_or_partner_age: 40 + # The deprecated shims keep their age-18 answers. + is_adult: [true, true, false] + family_type: COUPLE_WITH_CHILDREN + +- name: Without an identified parent an 18-year-old at school stays an adult + # "Living with parents" needs an identified parent; otherwise the model + # cannot tell a dependant from a partner and keeps the adult treatment. + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 24 + other: + age: 19 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [head, other] + households: + household: + members: [head, other] + output: + is_hbai_dependent_child: [false, false] + is_claimant_or_partner: [true, true] + relation_type: COUPLE + +- name: A 17-year-old living alone is a single adult, not their own child + period: 2025 + absolute_error_margin: 0 + input: + people: + young_person: + age: 17 + benunits: + benunit: + members: [young_person] + households: + household: + members: [young_person] + output: + is_hbai_dependent_child: false + is_hbai_adult: true + is_claimant_or_partner: true + hbai_person_type: WORKING_AGE_ADULT + relation_type: SINGLE + is_couple: false + is_single_person: true + is_lone_parent: false + # Deprecated shims: no one aged 18+, and the claimant counts as a child. + num_adults: 0 + family_type: LONE_PARENT + +- name: A 17-year-old lone parent with a baby + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 17 + is_parent: true + baby: + age: 0 + benunits: + benunit: + members: [parent, baby] + households: + household: + members: [parent, baby] + output: + is_claimant_or_partner: [true, false] + is_lone_parent: true + is_couple: false + youngest_claimant_or_partner_age: 17 + +- name: A couple with an 18-year-old at school stays a couple with a young person + period: 2025 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 40 + is_parent: true + parent_2: + age: 40 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent_1, parent_2, student] + households: + household: + members: [parent_1, parent_2, student] + output: + is_claimant_or_partner: [true, true, false] + relation_type: COUPLE + is_couple: true + is_single_person: false + is_lone_parent: false + is_married: true + marital_status: [MARRIED, MARRIED, SINGLE] + # Deprecated shim: three members aged 18+ are not "two adults". + family_type: SINGLE + +- name: A 16- or 17-year-old non-head, non-parent keeps the dependant treatment + # Flagged assumption: the FRS puts non-dependent 16- and 17-year-olds in their + # own benefit unit, so a non-head in that age group is treated as a dependant + # whatever their education inputs. + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 45 + teenager: + age: 17 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, teenager] + households: + household: + members: [parent, teenager] + output: + is_hbai_dependent_child: [false, true] + is_claimant_or_partner: [true, false] + +- name: A 19-year-old in higher education is an adult + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 45 + is_parent: true + student: + age: 19 + current_education: TERTIARY + benunits: + benunit: + members: [parent, student] + households: + household: + members: [parent, student] + output: + is_hbai_dependent_child: [false, false] + hbai_person_type: [WORKING_AGE_ADULT, WORKING_AGE_ADULT] + +- name: HBAI pensioners are adults at or above State Pension age + period: 2025 + absolute_error_margin: 0 + input: + people: + pensioner: + age: 70 + is_parent: true + young_pensioner: + age: 66 + grandchild: + age: 18 + current_education: POST_SECONDARY + benunits: + benunit: + members: [pensioner, young_pensioner, grandchild] + households: + household: + members: [pensioner, young_pensioner, grandchild] + output: + is_hbai_pensioner: [true, true, false] + is_hbai_working_age_adult: [false, false, false] + hbai_person_type: [PENSIONER, PENSIONER, CHILD] + +- name: HBAI equivalence-scale child bands + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + child_13: + age: 13 + child_14: + age: 14 + student_18: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, child_13, child_14, student_18] + households: + household: + members: [parent, child_13, child_14, student_18] + output: + is_hbai_child_under_14: [false, true, false, false] + is_hbai_child_aged_14_or_over: [false, false, true, true] + +# A benefit unit has one claimant or a couple, never more. Without relationship +# inputs, a member under 20 who is at least 16 years younger than the head is +# presumed the head's child, not their partner (a PolicyEngine presumption); +# any adults beyond a couple are neither claimant nor partner. + +- name: A couple with an 18-year-old in higher education stays a couple of two + period: 2025 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 42 + parent_2: + age: 40 + student: + age: 18 + benunits: + benunit: + members: [parent_1, parent_2, student] + households: + household: + members: [parent_1, parent_2, student] + output: + is_hbai_adult: [true, true, true] + is_claimant_or_partner: [true, true, false] + is_couple: true + marital_status: [MARRIED, MARRIED, SINGLE] + +- name: A lone parent with an 18-year-old in higher education is single + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + student: + age: 18 + toddler: + age: 3 + benunits: + benunit: + members: [parent, student, toddler] + households: + household: + members: [parent, student, toddler] + output: + is_claimant_or_partner: [true, false, false] + is_couple: false + +- name: A partner close in age is still a partner + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 40 + partner: + age: 22 + benunits: + benunit: + members: [head, partner] + households: + household: + members: [head, partner] + output: + # 22 is not under 20, so no presumption applies. + is_claimant_or_partner: [true, true] + is_couple: true + +- name: Three unrelated adults form one couple plus a non-dependant + period: 2025 + absolute_error_margin: 0 + input: + people: + eldest: + age: 50 + second: + age: 48 + third: + age: 45 + benunits: + benunit: + members: [eldest, second, third] + households: + household: + members: [eldest, second, third] + output: + is_claimant_or_partner: [true, true, false] + +- name: A supplied claimant-or-partner flag overrides the presumption + # Supplying the flag replaces the formula for everyone, so set it for both. + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 45 + is_claimant_or_partner: true + partner: + age: 19 + is_claimant_or_partner: true + benunits: + benunit: + members: [head, partner] + households: + household: + members: [head, partner] + output: + is_couple: true + +- name: With two explicit heads, the eldest adult head is the claimant + # Malformed input: two members flagged as head. The eldest is the claimant, + # the other adult head becomes the partner, and the 24-year-old is neither. + period: 2025 + absolute_error_margin: 0 + input: + people: + head_1: + age: 45 + is_benunit_head: true + head_2: + age: 43 + is_benunit_head: true + other: + age: 24 + benunits: + benunit: + members: [head_1, head_2, other] + households: + household: + members: [head_1, head_2, other] + output: + is_claimant_or_partner: [true, true, false] diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/equivalisation.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/equivalisation.yaml index 57f2d881cd..656d990f9e 100644 --- a/policyengine_uk/tests/policy/baseline/household/demographic/equivalisation.yaml +++ b/policyengine_uk/tests/policy/baseline/household/demographic/equivalisation.yaml @@ -119,3 +119,97 @@ output: household_equivalisation_bhc: 1.07 household_equivalisation_ahc: 0.98 + +# HBAI classifies people as adults or dependent children (HBAI FYE 2025 +# glossary), not by an age-18 cut-off. The two differ numerically only in +# households with no one aged 18 or over, because the scale gives other adults +# and children aged 14+ the same weight (0.33 BHC, 0.42 AHC). + +- name: Equivalisation factor — 17-year-old living alone is a single adult + # First adult only: 0.67 BHC, 0.58 AHC (previously 0.67 + 0.33 for a + # "child aged 14+", with no adult). + period: 2025 + absolute_error_margin: 0.001 + input: + people: + young_person: + age: 17 + benunits: + benunit: + members: [young_person] + households: + household: + members: [young_person] + output: + household_equivalisation_bhc: 0.67 + household_equivalisation_ahc: 0.58 + +- name: Equivalisation factor — 17-year-old lone parent with a baby + # BHC 0.67 + 0.20 = 0.87; AHC 0.58 + 0.20 = 0.78. + period: 2025 + absolute_error_margin: 0.001 + input: + people: + parent: + age: 17 + is_parent: true + baby: + age: 0 + benunits: + benunit: + members: [parent, baby] + households: + household: + members: [parent, baby] + output: + household_equivalisation_bhc: 0.87 + household_equivalisation_ahc: 0.78 + +- name: Equivalisation factor — two 17-year-olds sharing a household + # Each heads their own benefit unit, so both are adults: BHC 0.67 + 0.33 = + # 1.00; AHC 0.58 + 0.42 = 1.00. + period: 2025 + absolute_error_margin: 0.001 + input: + people: + sharer_1: + age: 17 + sharer_2: + age: 17 + benunits: + benunit_1: + members: [sharer_1] + benunit_2: + members: [sharer_2] + households: + household: + members: [sharer_1, sharer_2] + output: + household_equivalisation_bhc: 1.00 + household_equivalisation_ahc: 1.00 + +- name: Equivalisation factor — couple with an 18-year-old dependant at school + # The 18-year-old is an HBAI dependent child aged 14+: BHC 0.67 + 0.33 + 0.33 + # = 1.33; AHC 0.58 + 0.42 + 0.42 = 1.42 (unchanged from the age-18 split). + period: 2025 + absolute_error_margin: 0.001 + input: + people: + parent_1: + age: 45 + is_parent: true + parent_2: + age: 45 + is_parent: true + student: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent_1, parent_2, student] + households: + household: + members: [parent_1, parent_2, student] + output: + household_equivalisation_bhc: 1.33 + household_equivalisation_ahc: 1.42 diff --git a/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml b/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml index 0b70c7eeb7..c557150fb9 100644 --- a/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml +++ b/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml @@ -152,3 +152,36 @@ members: [high_earner, low_earner] output: marriage_allowance: [0, 0] + +- name: Marriage-neutral income tax — a dependant's income stays out of the couple's split + # Only the spouses' incomes are pooled: (60,000 + 10,000) / 2 = 35,000 each, + # which lowers their combined tax. The 19-year-old dependant at school keeps + # their own 8,000. Previously the dependant counted as a third adult, which + # blocked the split. + period: 2024 + absolute_error_margin: 1 + reforms: + - policyengine_uk.reforms.cps.marriage_tax_reforms.marriage_neutral_it_reform + input: + people: + high_earner: + age: 45 + is_parent: true + employment_income: 60_000 + low_earner: + age: 45 + is_parent: true + employment_income: 10_000 + student: + age: 19 + current_education: UPPER_SECONDARY + employment_income: 8_000 + benunits: + benunit: + members: [high_earner, low_earner, student] + is_married: true + households: + household: + members: [high_earner, low_earner, student] + output: + adjusted_net_income: [35_000, 35_000, 8_000] diff --git a/policyengine_uk/tests/test_carer_support_payment.py b/policyengine_uk/tests/test_carer_support_payment.py index b812935aa5..c8594b7581 100644 --- a/policyengine_uk/tests/test_carer_support_payment.py +++ b/policyengine_uk/tests/test_carer_support_payment.py @@ -1,3 +1,5 @@ +import pytest + from policyengine_uk import Simulation @@ -43,7 +45,10 @@ def test_scottish_carers_move_to_csp_in_2025(): assert sim.calculate("carer_support_payment", YEAR_2025)[0] > 0 -def test_csp_counts_for_pension_credit_carer_additions_and_blocks_severe_disability(): +def test_csp_counts_for_pension_credit_carer_additions(): + # A person's own carer benefit does not bar their severe disability + # addition: SPC Regs 2002 Sch I para 1(1)(a)(iii) bars it only where a + # carer benefit is paid to someone "in respect of caring for him". sim = Simulation( situation=_situation( YEAR_2025, @@ -51,17 +56,73 @@ def test_csp_counts_for_pension_credit_carer_additions_and_blocks_severe_disabil ) ) parameters = sim.tax_benefit_system.parameters(str(YEAR_2025)) + guarantee_credit = parameters.gov.dwp.pension_credit.guarantee_credit - expected_carer_addition = ( - float(parameters.gov.dwp.pension_credit.guarantee_credit.carer.addition) * 52 + expected_carer_addition = float(guarantee_credit.carer.addition) * 52 + expected_severe_disability_addition = ( + float(guarantee_credit.severe_disability.addition) * 52 ) assert sim.calculate("carer_minimum_guarantee_addition", YEAR_2025)[0] == ( expected_carer_addition ) - assert ( - sim.calculate("severe_disability_minimum_guarantee_addition", YEAR_2025)[0] == 0 + assert sim.calculate("severe_disability_minimum_guarantee_addition", YEAR_2025)[ + 0 + ] == pytest.approx(expected_severe_disability_addition) + + +def _couple_situation(year: int, claimant: dict, partner: dict): + return { + "people": { + "claimant": {"age": {year: 35}, **claimant}, + "partner": {"age": {year: 35}, **partner}, + }, + "benunits": {"benunit": {"members": ["claimant", "partner"]}}, + "households": { + "household": { + "members": ["claimant", "partner"], + "country": {year: "SCOTLAND"}, + } + }, + } + + +def test_partner_csp_for_caring_gives_couple_severe_disability_single_rate_or_nil(): + # Sch I para 1(1)(b): a couple qualifies only if both partners get a + # qualifying benefit; reg 6(5) then pays the single amount where a carer + # benefit is paid for one of them. Where only one partner qualifies and the + # other is not blind, para 1(1)(c) gives nothing. + both_qualify = _couple_situation( + YEAR_2025, + claimant={"attendance_allowance": {YEAR_2025: 1}}, + partner={ + "attendance_allowance": {YEAR_2025: 1}, + "care_hours": {YEAR_2025: 40}, + }, + ) + one_qualifies = _couple_situation( + YEAR_2025, + claimant={"attendance_allowance": {YEAR_2025: 1}}, + partner={"care_hours": {YEAR_2025: 40}}, + ) + sim = Simulation(situation=both_qualify) + parameters = sim.tax_benefit_system.parameters(str(YEAR_2025)) + single_amount = ( + float( + parameters.gov.dwp.pension_credit.guarantee_credit.severe_disability.addition + ) + * 52 ) + assert sim.calculate("carer_support_payment", YEAR_2025)[1] > 0 + assert sim.calculate("severe_disability_minimum_guarantee_addition", YEAR_2025)[ + 0 + ] == pytest.approx(single_amount) + + sim = Simulation(situation=one_qualifies) + assert sim.calculate("carer_support_payment", YEAR_2025)[1] > 0 + assert sim.calculate("severe_disability_minimum_guarantee_addition", YEAR_2025)[ + 0 + ] == pytest.approx(0) def test_csp_counts_for_uc_non_dep_exemption_and_housing_benefit_income(): diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py new file mode 100644 index 0000000000..2bb2f7413f --- /dev/null +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -0,0 +1,750 @@ +"""Programme definitions, benefit-unit structure and frozen age-18 shims. + +Invariants: + +1. For every age 0--25, education enum and combination of head, parent, + approved training, own-benefit receipt and looked-after status, the four + programmes' child predicates mean under 16. Their young-person predicates + agree with the statutory age/education/entry conditions the model encodes. + Legacy children are Child Benefit children/QYPs other than claimants, + partners and children placed by a local authority; the HBAI fallback and + the claimant-or-partner presumption follow their documented assumptions. +2. HBAI types partition everyone. Any benefit unit with one head has at most + two claimants/partners, all of them HBAI adults, and at least one if it + has an HBAI adult; valid families (one or two adults aged 20+ and + dependants) have exactly their adults. Couple/single and + couple/lone-parent/single-person partition benefit units. Heads aged 16+ + are claimants or partners, including when explicitly younger than others, + unless two other members are flagged parents and the head is not. +3. All 23 deprecated shims retain their original age-18 formulas, including + empty-group sentinels and historical eldest-child tie behaviour. Explicit + age_under_18 has the same membership as the shims for school-attendance + inputs and household adult age/capital-gains ranks (modelling conventions, + not statutory adult definitions). +4. Adding an under-16 dependant leaves existing claimant/partner membership + unchanged, even when the added child carries a parent marker (an + identified parent must be aged 16 or over; the minimised counterexample + that found this is kept as a regression test). + Before/after families share one Simulation to avoid repeated + model construction, but occupy distinct households and benefit units. + +Scope: these are tests of the ENCODED statutory limbs, not full entitlement. +Education status proxies full-time non-advanced study: hours, employment +contracts, approved providers and interruptions are unobserved. CB/CTC leaver +extensions are omitted, as is UC/PC's unconditional age-16-to-next-September +route. UC's terminal date is an input; PC's September terminal date is absent. +Own-benefit receipt collapses different programme-specific benefit lists. +UC/CTC/PC blanket looked-after exclusions omit statutory exceptions. HBAI's +fallback assumes dependence at 16--17 regardless of study, requires an +identified parent at 18--19 and infers partnership from head/parent status. +WTC childcare uses annual ages <16/<17 instead of the September/week cutoff; +disability inputs omit some statutory awards/history. Student support uses +benefit-unit membership as a proxy for financial dependence. These existing +limitations are preserved here, not treated as new model defects. + +Statutory texts and HBAI glossary were fetched on 2026-09-28; URLs accompany +the independent scalar references below. No network or survey data is used. +""" + +from itertools import product +from unittest.mock import patch + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.tax_benefit_system import system +from policyengine_uk.variables.household.demographic.highest_education import ( + EducationType, +) + +YEAR = 2026 +# Keep later mutations of the public module-level system out of these tests. +_PROPERTY_SYSTEM = system.clone() +EDUCATIONS = tuple(item.name for item in EducationType) +PROGRAMMES = ( + "child_benefit", + "universal_credit", + "child_tax_credit", + "pension_credit", +) +FLAGS = ( + "is_benunit_head", + "is_parent", + "is_in_approved_training", + "receives_benefits_in_own_right", + "is_looked_after_by_local_authority", +) +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +def person(age, **inputs): + return { + "age": age, + "current_education": "NOT_IN_EDUCATION", + "is_benunit_head": False, + "is_parent": False, + "is_in_approved_training": False, + "receives_benefits_in_own_right": False, + "is_looked_after_by_local_authority": False, + "is_apprentice": False, + "age_started_or_accepted_current_education_or_training": 17, + "is_before_universal_credit_qualifying_young_person_terminal_date": False, + "is_disabled_for_benefits": False, + "is_enhanced_disabled_for_benefits": False, + "is_severely_disabled_for_benefits": False, + "is_blind": False, + "is_SP_age": False, + "capital_gains_before_response": 0, + **inputs, + } + + +def simulate(families, year=YEAR, household_ids=None): + """One model construction for many independent benefit units.""" + situation = {"people": {}, "benunits": {}, "households": {}} + people = [] + for i, family in enumerate(families): + members = [] + for j, inputs in enumerate(family): + name = f"p{i}_{j}" + situation["people"][name] = {} + people.append(inputs) + members.append(name) + situation["benunits"][f"b{i}"] = {"members": members} + household = f"h{household_ids[i] if household_ids is not None else i}" + situation["households"].setdefault(household, {"members": []})[ + "members" + ].extend(members) + # Clone the real system, including independent parameters and variables. + # Reimporting every variable for each example creates thousands of new + # modules which Hypothesis then repeatedly scans for source constants. + with patch( + "policyengine_uk.simulation.CountryTaxBenefitSystem", + side_effect=_PROPERTY_SYSTEM.clone, + ): + sim = Simulation(situation=situation) + # Scalar situation parsing searches the person ID list for every input. + # Set whole columns once to keep the exhaustive Cartesian grid tractable. + variables = people[0].keys() + assert all(p.keys() == variables for p in people) + for variable in variables: + sim.set_input(variable, year, [p[variable] for p in people]) + # Constructor reforms calculate some family flags before these inputs + # exist. Clear those results, retaining only the supplied columns. This + # helper tests definitions, not reform amounts or baseline comparisons. + sim.input_variables = list(variables) + sim.reset_calculations() + return sim + + +def non_advanced_education(p): + # Encoded proxy for >12 hours' non-advanced education; the enum does not + # observe hours. https://www.legislation.gov.uk/uksi/2013/376/regulation/5 + return ( + p["current_education"] + not in { + "NOT_IN_EDUCATION", + "TERTIARY", + } + and not p["is_apprentice"] + ) + + +def statutory_child(age): + # SSCBA s142(1), WRA s40, TCA s8(3), SPC Regulations Sch IIA para 2: + # https://www.legislation.gov.uk/ukpga/1992/4/section/142 + # https://www.legislation.gov.uk/ukpga/2012/5/section/40 + # https://www.legislation.gov.uk/ukpga/2002/21/section/8 + # https://www.legislation.gov.uk/uksi/2002/1792/schedule/IIA + return age < 16 + + +def qualifying_young_person(p, programme): + # Education/training routes only (see omissions in module docstring): + # CB: https://www.legislation.gov.uk/uksi/2006/223/regulation/3 + # CB own-benefit restriction: https://www.legislation.gov.uk/uksi/2006/223/regulation/8 + # UC: https://www.legislation.gov.uk/uksi/2013/376/regulation/5 + # CTC: https://www.legislation.gov.uk/uksi/2002/2007/regulation/5 + # PC: https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A + if not 16 <= p["age"] < 20 or p["receives_benefits_in_own_right"]: + return False + if not (non_advanced_education(p) or p["is_in_approved_training"]): + return False + if ( + p["age"] >= 19 + and p["age_started_or_accepted_current_education_or_training"] >= 19 + ): + return False + if programme == "universal_credit" and p["age"] >= 19: + return p["is_before_universal_credit_qualifying_young_person_terminal_date"] + return True + + +def child_or_young_person(p, programme): + # Encoded responsibility filters, without their statutory exceptions: + # https://www.legislation.gov.uk/uksi/2013/376/regulation/4 + # https://www.legislation.gov.uk/uksi/2002/2007/regulation/3 + # https://www.legislation.gov.uk/uksi/2002/1792/schedule/IIA + eligible = statutory_child(p["age"]) or qualifying_young_person(p, programme) + if programme != "child_benefit" and p["is_looked_after_by_local_authority"]: + return False + return eligible + + +def hbai_dependent_child(p, family): + # Calculator fallback documented in is_hbai_dependent_child, with the + # deliberately limited household/education observations described above. + # https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#child + if p["age"] < 16: + return True + if p["is_benunit_head"] or p["is_parent"]: + return False + if p["age"] < 18: + return True + return ( + p["age"] < 20 + and any(member["is_parent"] for member in family) + and (non_advanced_education(p) or p["is_in_approved_training"]) + ) + + +def claimants_or_partners(family): + # is_claimant_or_partner's documented rule. The claimant is the head if an + # HBAI adult, else the eldest HBAI adult. The partner is the eldest other + # flagged parent, else the eldest other adult not presumed the claimant's + # child (under 20 and at least 16 years younger). If the claimant is not a + # flagged parent but two or more others are, the two eldest of those are + # the couple instead. Age ties go to the earlier member. + n = len(family) + adult = [not hbai_dependent_child(p, family) for p in family] + ages = [p["age"] for p in family] + heads = [i for i in range(n) if family[i]["is_benunit_head"] and adult[i]] + adults = [i for i in range(n) if adult[i]] + if not adults: + return [False] * n + pool = heads or adults + claimant = max(pool, key=lambda i: (ages[i], -i)) + parent = [adult[i] and family[i]["is_parent"] for i in range(n)] + other_parents = [i for i in range(n) if parent[i] and i != claimant] + if len(other_parents) >= 2 and not parent[claimant]: + couple = sorted(other_parents, key=lambda i: (-ages[i], i))[:2] + return [i in couple for i in range(n)] + pool = other_parents or [ + i + for i in adults + if i != claimant and not (ages[i] < 20 and ages[claimant] - ages[i] >= 16) + ] + partner = max(pool, key=lambda i: (ages[i], -i)) if pool else None + return [i == claimant or i == partner for i in range(n)] + + +def legacy_child_or_young_person(p, claimant): + # SSCBA s137 and IS reg14 / HB reg19, minus the claimant and partner and + # anyone placed by a local authority (IS reg16(4), HB reg21(3)): + # https://www.legislation.gov.uk/uksi/1987/1967/regulation/16 + # https://www.legislation.gov.uk/uksi/2006/213/regulation/21 + return ( + not claimant + and not p["is_looked_after_by_local_authority"] + and child_or_young_person(p, "child_benefit") + ) + + +def wtc_childcare_child(p, family): + # Annual proxy for reg14(3)-(4), not the precise September/week cutoff: + # https://www.legislation.gov.uk/uksi/2002/2005/regulation/14 + # https://www.legislation.gov.uk/uksi/2002/2007/regulation/3 + limit = 17 if p["is_disabled_for_benefits"] or p["is_blind"] else 16 + return ( + hbai_dependent_child(p, family) + and not p["is_looked_after_by_local_authority"] + and p["age"] < limit + ) + + +def assert_values(sim, variable, expected, year=YEAR): + actual = sim.calculate(variable, year) + if hasattr(actual, "decode_to_str"): + actual = actual.decode_to_str() + np.testing.assert_array_equal(actual, expected, err_msg=variable) + + +def assert_definitions(families, year=YEAR): + sim = simulate(families, year) + people = [p for family in families for p in family] + children = [statutory_child(p["age"]) for p in people] + # This is the membership input to private-school imputation. A calculator + # simulation's attends_private_school result is always zero and would + # therefore be a vacuous test of the imputation's age predicate. + under_18 = [p["age"] < 18 for p in people] + assert_values(sim, "age_under_18", under_18, year) + assert_values(sim, "is_child", under_18, year) + assert_values(sim, "is_adult", [not child for child in under_18], year) + assert_values( + sim, + "is_in_non_advanced_education", + [non_advanced_education(p) for p in people], + year, + ) + for programme in PROGRAMMES: + assert_values(sim, f"is_child_for_{programme}", children, year) + assert_values( + sim, + f"is_qualifying_young_person_for_{programme}", + [qualifying_young_person(p, programme) for p in people], + year, + ) + assert_values( + sim, + f"is_child_or_qualifying_young_person_for_{programme}", + [child_or_young_person(p, programme) for p in people], + year, + ) + hbai = [hbai_dependent_child(p, family) for family in families for p in family] + assert_values(sim, "is_hbai_dependent_child", hbai, year) + assert_values(sim, "is_hbai_adult", [not child for child in hbai], year) + claimants = [c for family in families for c in claimants_or_partners(family)] + assert_values(sim, "is_claimant_or_partner", claimants, year) + # SSCBA s137 and IS reg14 / HB reg19, projected to the encoded family: + # https://www.legislation.gov.uk/ukpga/1992/4/section/137 + # https://www.legislation.gov.uk/uksi/1987/1967/regulation/14 + # https://www.legislation.gov.uk/uksi/2006/213/regulation/19 + assert_values( + sim, + "is_child_or_young_person_for_legacy_benefits", + [legacy_child_or_young_person(p, c) for p, c in zip(people, claimants)], + year, + ) + assert_values( + sim, + "is_child_for_working_tax_credit_childcare_element", + [wtc_childcare_child(p, family) for family in families for p in family], + year, + ) + # Financial dependence is unobserved: test the documented membership proxy. + # https://www.legislation.gov.uk/uksi/2011/1986/regulation/42 + assert_values( + sim, + "is_dependent_child_for_student_support", + [not claimant for claimant in claimants], + year, + ) + assert_hbai_partition(sim, people, hbai, year) + assert_structure(sim, families, year) + + +def assert_hbai_partition(sim, people, hbai, year=YEAR): + working = [not child and not p["is_SP_age"] for p, child in zip(people, hbai)] + pensioners = [not child and p["is_SP_age"] for p, child in zip(people, hbai)] + assert_values(sim, "is_hbai_working_age_adult", working, year) + assert_values(sim, "is_hbai_pensioner", pensioners, year) + assert_values( + sim, + "hbai_person_type", + [ + "CHILD" if child else "PENSIONER" if retired else "WORKING_AGE_ADULT" + for child, retired in zip(hbai, pensioners) + ], + year, + ) + np.testing.assert_array_equal( + np.array(hbai, dtype=int) + np.array(working) + np.array(pensioners), 1 + ) + for variable, younger in ( + ("is_hbai_child_under_14", True), + ("is_hbai_child_aged_14_or_over", False), + ): + assert_values( + sim, + variable, + [child and ((p["age"] < 14) == younger) for p, child in zip(people, hbai)], + year, + ) + + +def assert_structure(sim, families, year=YEAR): + claimants = sim.calculate("is_claimant_or_partner", year) + couple = sim.calculate("is_couple", year) + single = sim.calculate("is_single", year) + lone = sim.calculate("is_lone_parent", year) + single_person = sim.calculate("is_single_person", year) + np.testing.assert_array_equal(couple ^ single, True) + np.testing.assert_array_equal( + couple.astype(int) + lone.astype(int) + single_person.astype(int), 1 + ) + offset = 0 + for i, family in enumerate(families): + expected = claimants_or_partners(family) + actual = claimants[offset : offset + len(family)] + np.testing.assert_array_equal(actual, expected) + adults = [not hbai_dependent_child(p, family) for p in family] + assert sum(actual) <= 2 + assert (sum(actual) >= 1) == any(adults) + assert all(adult for adult, claimant in zip(adults, actual) if claimant) + assert couple[i] == (sum(expected) >= 2) + responsible = any( + legacy_child_or_young_person(p, claimant) + for p, claimant in zip(family, expected) + ) + assert lone[i] == (sum(expected) < 2 and responsible) + # An adult head is the claimant unless two other flagged parents form + # the couple. + parents = [ + a and p["is_parent"] and not p["is_benunit_head"] + for p, a in zip(family, adults) + ] + for p, adult, claimant in zip(family, adults, actual): + if p["is_benunit_head"] and p["age"] >= 16: + assert adult + if sum(parents) < 2 or p["is_parent"]: + assert claimant + offset += len(family) + + +def test_every_age_education_and_structural_flag_combination(): + # 26 ages * 8 education values * 2**5 flags = 6,656 independent families. + # The matrix includes deliberately unusual inputs: an under-16 head or a + # student parent must not change the pure statutory under-16 definition. + families = [] + for age, education, flags in product( + range(26), EDUCATIONS, product((False, True), repeat=5) + ): + subject = person(age, current_education=education, **dict(zip(FLAGS, flags))) + parent = person( + 40, is_parent=True, is_benunit_head=not subject["is_benunit_head"] + ) + families.append([parent, subject]) + assert_definitions(families) + + +def test_entry_terminal_and_wtc_disability_boundaries(): + # Guarantee the age-19 entry/terminal boundaries and the distinct disabled + # and blind routes at WTC ages 15/16/17, independently of generated draws. + families = [] + for age, education, entry_age, flags in product( + range(15, 21), + ("POST_SECONDARY", "TERTIARY", "NOT_IN_EDUCATION"), + (18.5, 19, 1000), + product((False, True), repeat=5), + ): + apprentice, terminal, training, disabled, blind = flags + subject = person( + age, + current_education=education, + age_started_or_accepted_current_education_or_training=entry_age, + is_apprentice=apprentice, + is_before_universal_credit_qualifying_young_person_terminal_date=terminal, + is_in_approved_training=training, + is_disabled_for_benefits=disabled, + is_blind=blind, + ) + families.append([person(40, is_parent=True, is_benunit_head=True), subject]) + assert_definitions(families) + + +@PROPERTY_SETTINGS +@given( + st.fixed_dictionaries({flag: st.booleans() for flag in FLAGS}), + st.booleans(), + st.booleans(), + st.booleans(), + st.booleans(), + st.booleans(), + st.sampled_from([0, 16, 17, 18, 18.5, 19, 20, 25, 1000]), + st.integers(20, 90), + st.sampled_from([2024, 2026]), +) +def test_programme_definitions_with_generated_entry_and_household_context( + flags, + apprentice, + terminal, + disabled, + blind, + identified_parent, + entry_age, + parent_age, + year, +): + families = [] + for age, education in product(range(26), EDUCATIONS): + subject = person( + age, + current_education=education, + **flags, + is_apprentice=apprentice, + is_before_universal_credit_qualifying_young_person_terminal_date=terminal, + is_disabled_for_benefits=disabled, + is_blind=blind, + age_started_or_accepted_current_education_or_training=entry_age, + ) + parent = person( + parent_age, + is_parent=identified_parent, + is_benunit_head=not flags["is_benunit_head"], + is_SP_age=parent_age >= 66, + ) + families.append([parent, subject]) + assert_definitions(families, year) + + +@st.composite +def valid_families(draw): + families = [] + for _ in range(draw(st.integers(1, 8))): + adults = draw(st.lists(st.integers(20, 90), min_size=1, max_size=2)) + ages = draw(st.lists(st.integers(0, 19), max_size=5)) + family = [ + person( + age, is_parent=bool(ages), is_benunit_head=i == 0, is_SP_age=age >= 66 + ) + for i, age in enumerate(adults) + ] + for age in ages: + # The generator creates dependants, not additional claimants: + # 18--19-year-olds require education/training and a known parent. + education = draw(st.sampled_from(EDUCATIONS)) + training = draw(st.booleans()) + if age >= 18 and education in {"NOT_IN_EDUCATION", "TERTIARY"}: + training = True + family.append( + person( + age, current_education=education, is_in_approved_training=training + ) + ) + families.append(family) + return families + + +@PROPERTY_SETTINGS +@given(valid_families(), st.integers(0, 15)) +def test_valid_family_partition_and_adding_a_child_preserves_claimants( + families, added_age +): + # Keep every existing input fixed; even an unidentified parent stays so. + augmented = [family + [person(added_age)] for family in families] + combined = families + augmented + sim = simulate(combined) + assert_structure(sim, combined) + people = [p for family in combined for p in family] + hbai = [hbai_dependent_child(p, family) for family in combined for p in family] + assert_hbai_partition(sim, people, hbai) + claimants = sim.calculate("is_claimant_or_partner", YEAR) + old_offset = 0 + new_offset = sum(map(len, families)) + for family in families: + old = claimants[old_offset : old_offset + len(family)] + new = claimants[new_offset : new_offset + len(family)] + assert sum(old) == sum(p["age"] >= 20 for p in family) + assert sum(old) in (1, 2) + np.testing.assert_array_equal(new, old) + assert not claimants[new_offset + len(family)] + old_offset += len(family) + new_offset += len(family) + 1 + + +def test_calculated_heads_are_claimants_at_16_or_over(): + families = [[person(age)] for age in range(26)] + families += [[person(16), person(16)], [person(20), person(25)]] + # The default head is eldest, with ties resolved by member order. + for family in families: + for p in family: + del p["is_benunit_head"] + sim = simulate(families) + heads = sim.calculate("is_benunit_head", YEAR) + claimants = sim.calculate("is_claimant_or_partner", YEAR) + offset = 0 + for family in families: + expected_head = max(range(len(family)), key=lambda i: family[i]["age"]) + np.testing.assert_array_equal( + heads[offset : offset + len(family)], + [i == expected_head for i in range(len(family))], + ) + if family[expected_head]["age"] >= 16: + assert claimants[offset + expected_head] + offset += len(family) + + +@st.composite +def arbitrary_benefit_units(draw): + # Any ages, education, parent flags and head position: not only the + # shapes the FRS produces. + families = [] + for _ in range(draw(st.integers(1, 8))): + size = draw(st.integers(1, 6)) + head = draw(st.integers(0, size - 1)) + families.append( + [ + person( + draw(st.integers(0, 90)), + current_education=draw(st.sampled_from(EDUCATIONS)), + is_in_approved_training=draw(st.booleans()), + is_parent=draw(st.booleans()), + is_benunit_head=i == head, + ) + for i in range(size) + ] + ) + return families + + +@PROPERTY_SETTINGS +@given(arbitrary_benefit_units()) +def test_any_benefit_unit_has_at_most_two_claimants_or_partners(families): + assert_structure(simulate(families), families) + + +@st.composite +def shim_populations(draw): + members = st.fixed_dictionaries( + { + "age": st.integers(0, 90), + "current_education": st.sampled_from(EDUCATIONS), + "is_disabled_for_benefits": st.booleans(), + "is_enhanced_disabled_for_benefits": st.booleans(), + "is_severely_disabled_for_benefits": st.booleans(), + "is_SP_age": st.booleans(), + "person_id": st.integers(0, 20), + "capital_gains_before_response": st.integers(0, 100_000), + } + ) + raw = draw( + st.lists(st.lists(members, min_size=1, max_size=7), min_size=1, max_size=8) + ) + return [[person(**p) for p in family] for family in raw] + + +def frozen_rank(people, value, eligible, excluded): + """Stable descending rank, using Python sorting independently of core.""" + ordered = sorted( + (i for i, p in enumerate(people) if eligible(p)), + key=lambda i: -people[i][value], + ) + result = [excluded] * len(people) + for rank, i in enumerate(ordered, start=1): + result[i] = rank + return result + + +@PROPERTY_SETTINGS +@given(shim_populations()) +def test_all_deprecated_shims_and_explicit_age_ranks_keep_frozen_semantics(families): + # Always exercise both empty subgroups, all 0--25 age boundaries, stable + # ties, and the frozen distinction between exactly two and three adults. + families += [ + [person(age, person_id=age + 1) for age in range(26)], + [person(18, person_id=1)], + [person(18, person_id=1), person(18, person_id=2)], + [person(18, person_id=i) for i in range(3)], + [person(0, person_id=3), person(1, person_id=1), person(1, person_id=2)], + ] + # Two benefit units share each household: adult ranks must not restart at + # a benefit-unit boundary, whereas child ranks and counts must do so. + household_ids = [i // 2 for i in range(len(families))] + sim = simulate(families, household_ids=household_ids) + people = [p for family in families for p in family] + expected = { + "is_child": [p["age"] < 18 for p in people], + "is_adult": [p["age"] >= 18 for p in people], + "age_under_18": [p["age"] < 18 for p in people], + "is_WA_adult": [p["age"] >= 18 and not p["is_SP_age"] for p in people], + "is_young_child": [p["age"] < 14 for p in people], + "is_older_child": [14 <= p["age"] < 18 for p in people], + } + for family in families: + ages = [p["age"] for p in family] + children = [age for age in ages if age < 18] + adults = [age for age in ages if age >= 18] + group_values = { + "num_children": len(children), + "benunit_count_children": len(children), + "num_adults": len(adults), + "benunit_count_adults": len(adults), + "eldest_child_age": max(children, default=-np.inf), + "youngest_child_age": min(children, default=np.inf), + "eldest_adult_age": max(adults, default=-np.inf), + "youngest_adult_age": min(adults, default=np.inf), + "family_type": ( + "COUPLE_WITH_CHILDREN" if children else "COUPLE_NO_CHILDREN" + ) + if len(adults) == 2 + else ("LONE_PARENT" if children else "SINGLE"), + } + for suffix in ("disabled", "enhanced_disabled", "severely_disabled"): + for group, child in (("children", True), ("adults", False)): + group_values[f"num_{suffix}_{group}"] = sum( + ((p["age"] < 18) == child) and p[f"is_{suffix}_for_benefits"] + for p in family + ) + for variable, value in group_values.items(): + expected.setdefault(variable, []).append(value) + ranks = frozen_rank(family, "age", lambda p: p["age"] < 18, -1) + expected.setdefault("child_index", []).extend(ranks) + # Frozen oddity: every member of a childless family is "eldest child". + expected.setdefault("is_eldest_child", []).extend( + not children or rank == 1 for rank in ranks + ) + # Frozen oddity: a younger child's larger ID can leave tied eldest + # children with no selected eldest. Do not silently fix this shim. + eldest = max(children, default=-np.inf) + tied = children.count(eldest) > 1 + max_child_id = max(p["person_id"] if p["age"] < 18 else 0 for p in family) + expected.setdefault("is_benunit_eldest_child", []).extend( + p["age"] == eldest and (not tied or p["person_id"] == max_child_id) + for p in family + ) + households = {} + for household, family in zip(household_ids, families): + households.setdefault(household, []).extend(family) + for variable, value in ( + ("adult_index", "age"), + ("adult_index_cg", "capital_gains_before_response"), + ): + expected[variable] = [ + rank + for household in households.values() + for rank in frozen_rank(household, value, lambda p: p["age"] >= 18, 0) + ] + for variable, values in expected.items(): + assert_values(sim, variable, values) + + +@PROPERTY_SETTINGS +@given(st.integers(0, 15), st.sampled_from(EDUCATIONS), st.booleans()) +def test_adding_a_child_without_identifying_parents_preserves_existing_members( + added_age, education, training +): + families = [ + [ + person(24, is_benunit_head=True), + person(age, current_education=education, is_in_approved_training=training), + ] + for age in range(16, 20) + ] + combined = families + [family + [person(added_age)] for family in families] + sim = simulate(combined) + actual = sim.calculate("is_claimant_or_partner", YEAR) + for i in range(len(families)): + np.testing.assert_array_equal( + actual[2 * i : 2 * i + 2], actual[8 + 3 * i : 10 + 3 * i] + ) + + +def test_parent_marked_under_16_entrant_preserves_existing_claimants(): + # Executed and minimised: 20 is the smallest ordinary-adult age in our + # family generator, 18 the first affected student age, and 0 the smallest + # entrant age. Before: claimants [True, True]. After: [True, False, False]. + # The entrant is itself an HBAI dependent child despite its parent marker. + family = [ + person(20, is_benunit_head=True), + person(18, current_education="POST_SECONDARY"), + ] + sim = simulate([family, family + [person(0, is_parent=True)]]) + actual = sim.calculate("is_claimant_or_partner", YEAR) + np.testing.assert_array_equal(actual[:2], actual[2:4]) diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py index 717ee595ce..e32f8363e7 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -175,7 +175,10 @@ def situation( names = [] for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" - person = {"age": {YEAR: age}} + # The generated adults are the claimant and partner (a mixed-age + # couple may pair 67 with 18); say so, so the claimant-or-partner + # presumption does not apply. It must be set for everyone. + person = {"age": {YEAR: age}, "is_claimant_or_partner": {YEAR: True}} if age >= 67: person["state_pension"] = { YEAR: max(0.0, unit["state_pension"] + state_pension_change) @@ -193,7 +196,10 @@ def situation( names.append(name) for k, child_age in enumerate(unit.get("child_ages", [])): name = f"c{i}_{k}" - people[name] = {"age": {YEAR: child_age}} + people[name] = { + "age": {YEAR: child_age}, + "is_claimant_or_partner": {YEAR: False}, + } names.append(name) claim_pc = unit["would_claim_pc"] if would_claim_pc is None else would_claim_pc benunit = { @@ -213,6 +219,8 @@ def situation( people[name] = { "age": {YEAR: 40}, "employment_income": {YEAR: unit["non_dependant_income"]}, + # The claimant of their own benefit unit. + "is_claimant_or_partner": {YEAR: True}, } benunits[f"n{i}"] = { "members": [name], diff --git a/policyengine_uk/tests/test_dynamics_demographics.py b/policyengine_uk/tests/test_dynamics_demographics.py new file mode 100644 index 0000000000..56f85efb0f --- /dev/null +++ b/policyengine_uk/tests/test_dynamics_demographics.py @@ -0,0 +1,82 @@ +"""The labour supply dynamics split benefit units at age 18 explicitly. + +The OBR elasticity groups do not define a child, so the dynamics modules keep +an explicit age band instead of reading the deprecated generic child and adult +variables. This checks the helper reproduces those variables exactly. +""" + +import numpy as np + +from policyengine_uk import Simulation +from policyengine_uk.dynamics.demographics import benunit_age_18_composition + +SITUATION = { + "people": { + "parent": {"age": {"2025": 40}}, + "student": {"age": {"2025": 18}}, + "child_1": {"age": {"2025": 10}}, + "child_2": {"age": {"2025": 3}}, + "lodger": {"age": {"2025": 30}}, + "teen_head": {"age": {"2025": 17}}, + }, + "benunits": { + "family": {"members": ["parent", "student", "child_1", "child_2"]}, + "lodger": {"members": ["lodger"]}, + "teen": {"members": ["teen_head"]}, + }, + "households": { + "household": { + "members": [ + "parent", + "student", + "child_1", + "child_2", + "lodger", + "teen_head", + ] + } + }, +} + + +class _DefaultPeriod: + """Simulation.calculate with a default period, as the dynamics call it.""" + + def __init__(self, sim, period): + self.sim, self.period = sim, period + self.populations = sim.populations + + def calculate(self, variable, period=None, **kwargs): + return self.sim.calculate(variable, period or self.period, **kwargs) + + +def test_composition_matches_the_deprecated_age_18_variables(): + sim = Simulation(situation=SITUATION) + composition = benunit_age_18_composition(_DefaultPeriod(sim, 2025)) + np.testing.assert_array_equal( + composition["count_under_18"].values, + sim.calculate("benunit_count_children", 2025, map_to="person"), + ) + np.testing.assert_array_equal( + composition["youngest_under_18_age"].values, + sim.calculate("youngest_child_age", 2025, map_to="person"), + ) + np.testing.assert_array_equal( + composition["count_aged_18_or_over"].values, + sim.calculate("benunit_count_adults", 2025, map_to="person"), + ) + + +def test_composition_values(): + sim = Simulation(situation=SITUATION) + composition = benunit_age_18_composition(_DefaultPeriod(sim, 2025)) + assert composition["count_under_18"].tolist() == [2, 2, 2, 2, 0, 1] + assert composition["youngest_under_18_age"].tolist() == [ + 3, + 3, + 3, + 3, + np.inf, + 17, + ] + assert composition["count_aged_18_or_over"].tolist() == [2, 2, 2, 2, 1, 0] diff --git a/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py b/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py index 37672b3c3d..147b4e3fe9 100644 --- a/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py @@ -13,9 +13,9 @@ above the capital limit; and no family gets both Housing Benefit and Universal Credit. 2. Calculator mode (no reported benefits): a family is eligible exactly when - every adult is over State Pension age, it rents, and its capital is within - the limit; eligible families are paid their full entitlement (pensioners - are exempt from the benefit cap). + the claimant and any partner are over State Pension age, it rents, and its + capital is within the limit; eligible families are paid their full + entitlement (pensioners are exempt from the benefit cap). 3. Dataset mode (claims_all_entitled_benefits False, as in the FRS): Housing Benefit is paid only to reported claimants, and for families with a working-age adult eligibility equals the continuing-award rule (reported, @@ -25,9 +25,15 @@ 5. Metamorphic: a wholly pension-age family's Housing Benefit is non-increasing in private pension income. -A pensioner with an 18 or 19 year old dependant counts as having a working-age -adult for Housing Benefit, Pension Credit and Universal Credit alike, so the -mutual exclusion holds for that shape too. +A pensioner with an 18 or 19 year old dependant is a pension-age claimant for +Housing Benefit, Pension Credit and Universal Credit alike (the dependant is +not a claimant or partner), so that family can claim Housing Benefit and not +Universal Credit, and the mutual exclusion holds for that shape too. + +Every member's is_claimant_or_partner is set from its generated role, as the +FRS supplies it. Without that input an 18- or 19-year-old partner of a +pensioner would be presumed the pensioner's child; that presumption is tested +in test_child_and_adult_definitions_properties.py. """ import numpy as np @@ -90,7 +96,12 @@ def situation(units, claims_all=None, flip_would_claim_uc=False, pension_bump=0. names = [] for j, (role, age) in enumerate(unit["members"]): name = f"p{i}_{j}" - person = {"age": {YEAR: age}} + # Setting the input for anyone makes it an input for everyone, so + # set it for every member of every family. + person = { + "age": {YEAR: age}, + "is_claimant_or_partner": {YEAR: role != "dependant"}, + } if role != "dependant" and age >= 67: person["state_pension_reported"] = {YEAR: unit["state_pension"]} person["private_pension_income"] = { @@ -151,7 +162,8 @@ def calculate(units, **kwargs): def wholly_pension_age(unit): - return all(role != "dependant" and age >= 67 for role, age in unit["members"]) + # The claimant and any partner, not dependants (SI 2014/1230 reg 6A(4)). + return all(age >= 67 for role, age in unit["members"] if role != "dependant") def check_structural(values): diff --git a/policyengine_uk/tests/test_means_test_income_properties.py b/policyengine_uk/tests/test_means_test_income_properties.py new file mode 100644 index 0000000000..fcfb8f56a2 --- /dev/null +++ b/policyengine_uk/tests/test_means_test_income_properties.py @@ -0,0 +1,123 @@ +"""A means test never counts the income of someone outside its family. + +Invariant: for any benefit unit, giving income to a member who is neither the +claimant, the partner, nor the programme's own child or young person (for +example an 18- or 19-year-old presumed the claimant's child but not in +education, or a third adult) never changes a means test's income. The UC, HB, +IS, tax credit, CTR and Pension Credit income totals are covered. + +Each example builds families twice in one simulation: once with the other +member's income and once without, in separate households and benefit units. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2024 +INCOME_TESTS = [ + "uc_earned_income", + "uc_unearned_income", + "housing_benefit_applicable_income", + "income_support_applicable_income", + "tax_credits_applicable_income", + "council_tax_reduction_applicable_income", + "pension_credit_income", +] +EDUCATIONS = ["NOT_IN_EDUCATION", "UPPER_SECONDARY", "TERTIARY"] +FAMILY_FLAGS = [ + "is_claimant_or_partner", + "is_child_or_qualifying_young_person_for_universal_credit", + "is_child_or_young_person_for_legacy_benefits", + "is_child_or_qualifying_young_person_for_child_tax_credit", +] +money = st.floats(0, 50_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def families(draw): + # A head, an optional partner, and one member who by construction is + # outside every programme's family: a 16-19-year-old at least 16 years + # younger than the head (presumed the head's child) who is not in + # non-advanced education, so no programme's young person; or a third adult + # no older than the partner (the partner, earlier in member order, wins any + # age tie). + kind = draw(st.sampled_from(["presumed_child", "third_adult"])) + if kind == "presumed_child": + other_age = draw(st.integers(16, 19)) + head_age = draw(st.integers(other_age + 16, 85)) + education = draw(st.sampled_from(["NOT_IN_EDUCATION", "TERTIARY"])) + else: + head_age = draw(st.integers(25, 85)) + education = draw(st.sampled_from(EDUCATIONS)) + family = [ + { + "age": head_age, + "employment_income": draw(money), + "private_pension_income": draw(money), + } + ] + if kind == "third_adult" or draw(st.booleans()): + partner_age = draw(st.integers(20, head_age)) + family.append({"age": partner_age, "employment_income": draw(money)}) + if kind == "third_adult": + other_age = draw(st.integers(20, partner_age)) + family.append( + { + "age": other_age, + "current_education": education, + "employment_income": draw(st.floats(1, 50_000)), + "private_pension_income": draw(money), + "savings_interest_income": draw(money), + } + ) + return family + + +def situation(families_): + people, benunits, households = {}, {}, {} + for i, family in enumerate(families_): + names = [] + for j, inputs in enumerate(family): + name = f"p{i}_{j}" + people[name] = {k: {YEAR: v} for k, v in inputs.items()} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = {"members": names} + return {"people": people, "benunits": benunits, "households": households} + + +@settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(families(), min_size=1, max_size=6)) +def test_income_of_non_claimants_never_counts(units): + without = [ + family[:-1] + + [ + { + **family[-1], + "employment_income": 0, + "private_pension_income": 0, + "savings_interest_income": 0, + } + ] + for family in units + ] + sim = Simulation(situation=situation(units + without)) + offsets = np.cumsum([0] + [len(f) for f in units]) + n = len(units) + for variable in FAMILY_FLAGS: + flags = sim.calculate(variable, YEAR) + for i in range(n): + # The generator's construction, checked against the model. + assert not flags[offsets[i + 1] - 1], (variable, units[i]) + for variable in INCOME_TESTS: + values = sim.calculate(variable, YEAR) + for i in range(n): + assert abs(values[i] - values[n + i]) < 0.01, (variable, units[i]) diff --git a/policyengine_uk/tests/test_uc_unearned_benefits_properties.py b/policyengine_uk/tests/test_uc_unearned_benefits_properties.py index 4a2cba3ae6..1b04d59149 100644 --- a/policyengine_uk/tests/test_uc_unearned_benefits_properties.py +++ b/policyengine_uk/tests/test_uc_unearned_benefits_properties.py @@ -117,7 +117,11 @@ def situation(units, year, income_variable, bump=0.0, carer=False): names = [] for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" - person = {"age": {year: age}} + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a much younger member is the + # head's child) does not apply. Setting it for anyone makes it an + # input for everyone, so children get False below. + person = {"age": {year: age}, "is_claimant_or_partner": {year: True}} if j == 0: if carer: person["care_hours"] = {year: 40} @@ -129,7 +133,7 @@ def situation(units, year, income_variable, bump=0.0, carer=False): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { diff --git a/policyengine_uk/utils/benefit_unit.py b/policyengine_uk/utils/benefit_unit.py new file mode 100644 index 0000000000..853726105f --- /dev/null +++ b/policyengine_uk/utils/benefit_unit.py @@ -0,0 +1,21 @@ +"""Benefit-unit aggregation helpers for means tests.""" + + +def add_for_members(benunit, period, variables, members): + """Sum variables over the benefit-unit members a means test counts. + + ``members`` is a person-level boolean array. Person-level variables are + summed over those members only; benefit-unit variables are added as they + are. Means tests pass the claimant and partner plus the programme's own + children or young persons, so a member who is none of these (for example + an 18-year-old who is neither a partner nor a qualifying young person) + does not count. + """ + person = benunit.members + total = 0 + for name in variables: + if person.entity.get_variable(name).entity.is_person: + total = total + benunit.sum(person(name, period) * members) + else: + total = total + benunit(name, period) + return total diff --git a/policyengine_uk/variables/contrib/labour/attends_private_school.py b/policyengine_uk/variables/contrib/labour/attends_private_school.py index 27cc135f61..6bd463ec02 100644 --- a/policyengine_uk/variables/contrib/labour/attends_private_school.py +++ b/policyengine_uk/variables/contrib/labour/attends_private_school.py @@ -48,7 +48,9 @@ def formula(person, period, parameters): person = household.members - is_child = person("is_child", period) + # No law governs this imputation. The attendance rates apply to + # everyone under 18, so the pool is an explicit age band. + under_18 = person("age_under_18", period) taxes = household.sum( person("income_tax", period) + person("national_insurance", period) @@ -90,7 +92,7 @@ def formula(person, period, parameters): p_attends_private_school = ( _rate_by_percentile[percentile] * STUDENT_POPULATION_ADJUSTMENT_FACTOR - * is_child + * under_18 ) # Use pre-generated random draw from dataset instead of calling random() diff --git a/policyengine_uk/variables/contrib/labour/private_school_vat.py b/policyengine_uk/variables/contrib/labour/private_school_vat.py index 33b641d450..d6e7823eec 100644 --- a/policyengine_uk/variables/contrib/labour/private_school_vat.py +++ b/policyengine_uk/variables/contrib/labour/private_school_vat.py @@ -9,7 +9,7 @@ class private_school_vat(Variable): unit = "currency-GBP" def formula(household, period, parameters): - num_children = add(household, period, ["attends_private_school"]) + num_private_school_pupils = add(household, period, ["attends_private_school"]) ps_vat_params = parameters(period).gov.simulation.private_school_vat private_school_vat_basis = ps_vat_params.private_school_vat_basis @@ -20,7 +20,7 @@ def formula(household, period, parameters): ).gov.contrib.labour.private_school_vat return ( - num_children + num_private_school_pupils * avg_yearly_private_school_cost * private_school_vat_rate * private_school_vat_basis diff --git a/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py b/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py index 8e5d70a2eb..ccc4d49f1d 100644 --- a/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py +++ b/policyengine_uk/variables/gov/dfe/care_to_learn/care_to_learn_eligible.py @@ -7,6 +7,10 @@ class care_to_learn_eligible(Variable): label = "eligible for Care to Learn childcare support" definition_period = YEAR defined_for = "would_claim_care_to_learn" + reference = ( + "https://www.gov.uk/government/publications/care-to-learn-conditions-of-grant-funding/care-to-learn-academic-year-2026-to-2027-conditions-of-grant-funding", + "https://www.legislation.gov.uk/ukpga/1992/4/section/142", + ) def formula(person, period, parameters): # Link for instruction: https://www.gov.uk/care-to-learn/eligibility @@ -14,8 +18,14 @@ def formula(person, period, parameters): # Only parents can be eligible, not children is_parent = person("is_parent", period) - # Check basic eligibility conditions - benunit_has_children = person.benunit.any(person("is_child", period)) + # The young parent must be the main carer of, and receive Child + # Benefit for, the child they claim for (ESFA conditions of grant). + # So the child is a Child Benefit child or qualifying young person in + # the benefit unit, other than the parent themselves. + cares_for_child_benefit_child = person.benunit.any( + person("is_child_or_qualifying_young_person_for_child_benefit", period) + & ~is_parent + ) p = parameters(period).gov.dfe.care_to_learn age_eligible = person("age", period) < p.age_limit @@ -34,7 +44,7 @@ def formula(person, period, parameters): return ( is_parent - & benunit_has_children + & cares_for_child_benefit_child & age_eligible & not_higher_education & lives_in_england diff --git a/policyengine_uk/variables/gov/dfe/childcare_grant/childcare_grant_child_eligible.py b/policyengine_uk/variables/gov/dfe/childcare_grant/childcare_grant_child_eligible.py index d065ee4b9a..36a2b5e9b1 100644 --- a/policyengine_uk/variables/gov/dfe/childcare_grant/childcare_grant_child_eligible.py +++ b/policyengine_uk/variables/gov/dfe/childcare_grant/childcare_grant_child_eligible.py @@ -6,10 +6,13 @@ class childcare_grant_child_eligible(Variable): entity = Person label = "Child is eligible for Childcare Grant" documentation = ( - "Whether the child satisfies the Childcare Grant child-age condition." + "Whether a dependent child of the student satisfies the Childcare " + "Grant age condition: under 15, or under 17 with special educational " + "needs, immediately before the academic year begins." ) definition_period = YEAR - defined_for = "is_child" + defined_for = "is_dependent_child_for_student_support" + reference = "https://www.legislation.gov.uk/uksi/2011/1986/regulation/45" def formula(person, period, parameters): p = parameters(period).gov.dfe.childcare_grant.eligible_child.max_age diff --git a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py index dc2ffb606a..0e5f5bc408 100644 --- a/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py +++ b/policyengine_uk/variables/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_eligible.py @@ -7,6 +7,11 @@ class extended_childcare_entitlement_eligible(Variable): label = "eligibility for extended childcare entitlement" definition_period = YEAR defined_for = "would_claim_extended_childcare" + reference = ( + "https://www.legislation.gov.uk/ukpga/2016/5/section/1", + "https://www.legislation.gov.uk/uksi/2022/1134/regulation/14", + "https://www.legislation.gov.uk/uksi/2022/1134/regulation/15", + ) def formula(benunit, period, parameters): # Check if household is in England @@ -14,12 +19,14 @@ def formula(benunit, period, parameters): countries = country.possible_values in_england = country == countries.ENGLAND - # Check income condition - must be true for all family members (except children) + # The income conditions apply to the parent and the parent's partner + # (Childcare Act 2016 s.1(2)(d); SI 2022/1134 regs 14, 15 and 18): + # the claimant and partner of the benefit unit, not their children. person = benunit.members person_meets_income_condition = person( "extended_childcare_entitlement_meets_income_requirements", period, - ) | person("is_child", period) + ) | ~person("is_claimant_or_partner", period) meets_income_condition = benunit.all(person_meets_income_condition) # Check work condition diff --git a/policyengine_uk/variables/gov/dfe/is_dependent_child_for_student_support.py b/policyengine_uk/variables/gov/dfe/is_dependent_child_for_student_support.py new file mode 100644 index 0000000000..bfbc366ec4 --- /dev/null +++ b/policyengine_uk/variables/gov/dfe/is_dependent_child_for_student_support.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class is_dependent_child_for_student_support(Variable): + """Dependent child of a student for student support in England. + + A dependent child is a child of the student or of their partner, or one the + student has parental responsibility for, who is wholly or mainly + financially dependent on the student (Education (Student Support) + Regulations 2011 reg 42(1)). The regulations set no age limit; each grant + applies its own (for example, Childcare Grant ages under reg 45(2)). + + Financial dependence is not observed, so the children and young persons in + the student's benefit unit (everyone other than the claimant or partner) + stand in for it. + """ + + value_type = bool + entity = Person + label = "Dependent child for student support" + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2011/1986/regulation/42" + + def formula(person, period, parameters): + return ~person("is_claimant_or_partner", period) diff --git a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_has_sponsor.py b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_has_sponsor.py index 338ac9ea64..cc69cb761d 100644 --- a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_has_sponsor.py +++ b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_has_sponsor.py @@ -13,10 +13,9 @@ def formula(person, period, parameters): age = person("age", period) in_higher_education = person("maintenance_loan_in_higher_education", period) adult_index = person("adult_index", period) - is_adult = person("is_adult", period) in_he = in_higher_education | person("in_HE", period) - sponsor_candidate = is_adult & np.logical_not(in_he) & (age >= 29) + sponsor_candidate = np.logical_not(in_he) & (age >= 29) head_candidate = sponsor_candidate & (adult_index == 1) second_candidate = sponsor_candidate & (adult_index == 2) diff --git a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_sponsor_income.py b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_sponsor_income.py index f4789f2f1b..02e89cf2a0 100644 --- a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_sponsor_income.py +++ b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_sponsor_income.py @@ -17,11 +17,10 @@ def formula(person, period, parameters): age = person("age", period) in_higher_education = person("maintenance_loan_in_higher_education", period) adult_index = person("adult_index", period) - is_adult = person("is_adult", period) in_he = in_higher_education | person("in_HE", period) benunit_income = person("maintenance_loan_candidate_benunit_income", period) - sponsor_candidate = is_adult & np.logical_not(in_he) & (age >= 29) + sponsor_candidate = np.logical_not(in_he) & (age >= 29) head_candidate = sponsor_candidate & (adult_index == 1) second_candidate = sponsor_candidate & (adult_index == 2) diff --git a/policyengine_uk/variables/gov/dfe/parents_learning_allowance/parents_learning_allowance_dependent_children.py b/policyengine_uk/variables/gov/dfe/parents_learning_allowance/parents_learning_allowance_dependent_children.py index b2e9ccfadf..f7c650fb47 100644 --- a/policyengine_uk/variables/gov/dfe/parents_learning_allowance/parents_learning_allowance_dependent_children.py +++ b/policyengine_uk/variables/gov/dfe/parents_learning_allowance/parents_learning_allowance_dependent_children.py @@ -6,10 +6,11 @@ class parents_learning_allowance_dependent_children(Variable): entity = BenUnit label = "Dependent children for Parents' Learning Allowance" documentation = ( - "Number of children in the student's benefit unit counted as dependent children for the first-pass " - "Parents' Learning Allowance model." + "Number of dependent children of the student (Education (Student " + "Support) Regulations 2011 regs 42 and 46). The law sets no age limit." ) definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2011/1986/regulation/46" def formula(benunit, period, parameters): - return benunit.sum(benunit.members("is_child", period)) + return add(benunit, period, ["is_dependent_child_for_student_support"]) diff --git a/policyengine_uk/variables/gov/dwp/LHA_category.py b/policyengine_uk/variables/gov/dwp/LHA_category.py index dde91060fa..f0f2d4eb76 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_category.py +++ b/policyengine_uk/variables/gov/dwp/LHA_category.py @@ -27,17 +27,8 @@ def formula(benunit, period, parameters): person = benunit.members household = person.household is_shared = benunit.any(household("is_shared_accommodation", period.this_year)) - num_adults_in_hh = benunit.max(household.sum(person("is_adult", period))) - eldest_adult_age_in_hh = benunit.max(household.max(person("age", period))) - has_children = benunit.any(person("is_child", period)) - # Households with only one adult, if under age threshold, can only - # claim shared if without children: - # https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 - p = parameters(period).gov.dwp.LHA - can_only_claim_shared = ( - (num_adults_in_hh == 1) - & (eldest_adult_age_in_hh < p.shared_accommodation_age_threshold) - & ~has_children + can_only_claim_shared = benunit( + "is_lha_shared_accommodation_rate_specified_renter", period ) return select( [ diff --git a/policyengine_uk/variables/gov/dwp/WTC_childcare_element.py b/policyengine_uk/variables/gov/dwp/WTC_childcare_element.py index 9bff287019..cbc91abd86 100644 --- a/policyengine_uk/variables/gov/dwp/WTC_childcare_element.py +++ b/policyengine_uk/variables/gov/dwp/WTC_childcare_element.py @@ -6,15 +6,20 @@ class WTC_childcare_element(Variable): entity = BenUnit label = "Working Tax Credit childcare element" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 11" + reference = ( + "https://www.legislation.gov.uk/uksi/2002/2005/regulation/14", + "https://www.legislation.gov.uk/uksi/2002/2005/regulation/20", + ) unit = GBP defined_for = "is_WTC_eligible" def formula(benunit, period, parameters): WTC = parameters(period).gov.dwp.tax_credits.working_tax_credit - num_children = benunit("num_children", period) - childcare_1 = (num_children == 1) * WTC.elements.childcare_1 - childcare_2 = (num_children > 1) * WTC.elements.childcare_2 + num_childcare_children = add( + benunit, period, ["is_child_for_working_tax_credit_childcare_element"] + ) + childcare_1 = (num_childcare_children == 1) * WTC.elements.childcare_1 + childcare_2 = (num_childcare_children > 1) * WTC.elements.childcare_2 max_childcare_amount = (childcare_1 + childcare_2) * WEEKS_IN_YEAR expenses = add(benunit, period, ["childcare_expenses"]) eligible_expenses = min_(max_childcare_amount, expenses) diff --git a/policyengine_uk/variables/gov/dwp/WTC_couple_element.py b/policyengine_uk/variables/gov/dwp/WTC_couple_element.py index 08350266d0..988bc468ec 100644 --- a/policyengine_uk/variables/gov/dwp/WTC_couple_element.py +++ b/policyengine_uk/variables/gov/dwp/WTC_couple_element.py @@ -6,7 +6,7 @@ class WTC_couple_element(Variable): entity = BenUnit label = "Working Tax Credit couple element" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 11" + reference = "https://www.legislation.gov.uk/uksi/2002/2005/regulation/11" unit = GBP defined_for = "is_WTC_eligible" diff --git a/policyengine_uk/variables/gov/dwp/WTC_disabled_element.py b/policyengine_uk/variables/gov/dwp/WTC_disabled_element.py index 9bbaf94f7d..958bc65d3c 100644 --- a/policyengine_uk/variables/gov/dwp/WTC_disabled_element.py +++ b/policyengine_uk/variables/gov/dwp/WTC_disabled_element.py @@ -6,7 +6,7 @@ class WTC_disabled_element(Variable): entity = BenUnit label = "Working Tax Credit disabled element" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 11" + reference = "https://www.legislation.gov.uk/uksi/2002/2005/regulation/9" unit = GBP defined_for = "is_WTC_eligible" @@ -17,7 +17,7 @@ def formula(benunit, period, parameters): person_qualifies = ( person_meets_hours & person("is_disabled_for_benefits", period) - & person("is_adult", period) + & person("is_claimant_or_partner", period) ) qualifies = benunit.any(person_qualifies) return qualifies * WTC.elements.disabled diff --git a/policyengine_uk/variables/gov/dwp/WTC_lone_parent_element.py b/policyengine_uk/variables/gov/dwp/WTC_lone_parent_element.py index 8500415a73..62bdd91cef 100644 --- a/policyengine_uk/variables/gov/dwp/WTC_lone_parent_element.py +++ b/policyengine_uk/variables/gov/dwp/WTC_lone_parent_element.py @@ -6,13 +6,16 @@ class WTC_lone_parent_element(Variable): entity = BenUnit label = "Working Tax Credit lone parent element" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 11" + reference = "https://www.legislation.gov.uk/uksi/2002/2005/regulation/12" unit = GBP defined_for = "is_WTC_eligible" def formula(benunit, period, parameters): WTC = parameters(period).gov.dwp.tax_credits.working_tax_credit - family_type = benunit("family_type", period) - families = family_type.possible_values - lone_parent = family_type == families.LONE_PARENT + # A single claim by a claimant responsible for a child or qualifying + # young person (reg 12). + lone_parent = benunit("is_single", period) & benunit( + "is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit", + period, + ) return lone_parent * WTC.elements.lone_parent diff --git a/policyengine_uk/variables/gov/dwp/WTC_severely_disabled_element.py b/policyengine_uk/variables/gov/dwp/WTC_severely_disabled_element.py index d1bc2cfe36..1322ed9e01 100644 --- a/policyengine_uk/variables/gov/dwp/WTC_severely_disabled_element.py +++ b/policyengine_uk/variables/gov/dwp/WTC_severely_disabled_element.py @@ -6,13 +6,17 @@ class WTC_severely_disabled_element(Variable): entity = BenUnit label = "Working Tax Credit severely disabled element" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 11" + reference = "https://www.legislation.gov.uk/uksi/2002/2005/regulation/17" unit = GBP def formula(benunit, period, parameters): WTC = parameters(period).gov.dwp.tax_credits.working_tax_credit - amount = ( - benunit("num_severely_disabled_adults", period) - * WTC.elements.severely_disabled + person = benunit.members + # The claimant, or either joint claimant, who is severely disabled + # (reg 17); a severely disabled child counts for CTC instead. + severely_disabled_claimants = benunit.sum( + person("is_claimant_or_partner", period) + & person("is_severely_disabled_for_benefits", period) ) + amount = severely_disabled_claimants * WTC.elements.severely_disabled return benunit("is_WTC_eligible", period) * amount diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap.py b/policyengine_uk/variables/gov/dwp/benefit_cap.py index 2f53751034..f67e218041 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap.py @@ -9,9 +9,7 @@ class benefit_cap(Variable): unit = GBP def formula(benunit, period, parameters): - is_single_adult = benunit("num_adults", period) == 1 - has_children = benunit("num_children", period) > 0 - single_claimant = is_single_adult & ~has_children + single_claimant = benunit("is_benefit_cap_single_claimant_rate", period) household_region = benunit.members.household("region", period) region = benunit.value_from_first_person(household_region) regions = household_region.possible_values @@ -32,4 +30,4 @@ def formula(benunit, period, parameters): ], ) exempt = benunit("is_benefit_cap_exempt", period) - return where(exempt, np.inf * np.ones_like(has_children), rate) + return where(exempt, np.inf, rate) diff --git a/policyengine_uk/variables/gov/dwp/esa_income_assessable_capital.py b/policyengine_uk/variables/gov/dwp/esa_income_assessable_capital.py index 89cd48f57d..5487fc42a9 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_assessable_capital.py @@ -10,15 +10,18 @@ class esa_income_assessable_capital(Variable): "ESA capital test. Because the dataset only stores these stocks at " "household level, the model allocates full household capital to any " "benunit with a reported income-related ESA award and only falls back to " - "an adult-share proxy when nobody in the household is on that reported " - "claim path. " + "a claimant-and-partner share when nobody in the household is on that " + "reported claim path. This allocation is a PolicyEngine convention; " + "the data cannot identify ownership of capital. " "Person-level sources, such as a Lifetime ISA, count only for the " "holder's own benunit, and only when the holder is its claimant or " - "partner (is_uc_claimant): a dependant's capital is not the claimant's." + "partner (is_claimant_or_partner): a dependant's capital is not the " + "claimant's." ) definition_period = YEAR unit = GBP quantity_type = STOCK + reference = "https://www.legislation.gov.uk/uksi/2008/794/regulation/83" def formula(benunit, period, parameters): ESA = parameters(period).gov.dwp.ESA.income @@ -29,19 +32,23 @@ def formula(benunit, period, parameters): household_capital = sum( benunit.max(person.household(source, period)) for source in sources ) - benunit_adults = add(benunit, period, ["is_adult"]) + # Regulation 83(2) excludes children's and young persons' capital. + # The claimant/partner weights approximate otherwise unobserved ownership. + benunit_claimants_and_partners = add( + benunit, period, ["is_claimant_or_partner"] + ) household_reporting_claimants = benunit.max( - person.household.sum( - person("is_adult", period) & (person("esa_income_reported", period) > 0) - ) + person.household.sum(person("esa_income_reported", period) > 0) ) - household_adults = benunit.max( - person.household.sum(person.household.members("is_adult", period)) + household_claimants_and_partners = benunit.max( + person.household.sum(person("is_claimant_or_partner", period)) ) - fallback_divisor = max_(1, household_adults) + fallback_divisor = max_(1, household_claimants_and_partners) claiming_proxy = where(claiming_esa_income, household_capital, 0) - fallback_proxy = household_capital * benunit_adults / fallback_divisor - claimant_or_partner = person("is_uc_claimant", period) + fallback_proxy = ( + household_capital * benunit_claimants_and_partners / fallback_divisor + ) + claimant_or_partner = person("is_claimant_or_partner", period) person_capital = sum( benunit.sum(person(source, period) * claimant_or_partner) for source in ESA.capital.person_sources diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 999da75d2a..f07a386d61 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -9,6 +9,14 @@ class housing_benefit_applicable_income(Variable): unit = GBP 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 (HB Regs 2006 reg 25); dropping dependants' + # own income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_young_person_for_legacy_benefits", period + ) any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) BENUNIT_MEANS_TESTED_BENEFITS = [ "child_benefit", @@ -35,19 +43,25 @@ def formula(benunit, period, parameters): ] bi = parameters(period).gov.contrib.ubi_center.basic_income # Add personal benefits, credits and total benefits to income - benefits = add(benunit, period, BENUNIT_MEANS_TESTED_BENEFITS) - income = add(benunit, period, INCOME_COMPONENTS) - personal_benefits = add(benunit, period, PERSONAL_BENEFITS) - credits = add(benunit, period, ["tax_credits"]) + benefits = add_for_members( + benunit, period, BENUNIT_MEANS_TESTED_BENEFITS, members + ) + income = add_for_members(benunit, period, INCOME_COMPONENTS, members) + personal_benefits = add_for_members(benunit, period, PERSONAL_BENEFITS, members) + credits = add_for_members(benunit, period, ["tax_credits"], members) increased_income = income + personal_benefits + credits + benefits if not bi.interactions.include_in_means_tests: # Basic income is already in personal benefits, deduct if needed - increased_income -= add(benunit, period, ["basic_income"]) + increased_income -= add_for_members( + benunit, period, ["basic_income"], members + ) # Reduce increased income by pension contributions and tax - pension_contributions = add(benunit, period, ["pension_contributions"]) * 0.5 + pension_contributions = ( + add_for_members(benunit, period, ["pension_contributions"], members) * 0.5 + ) TAX_COMPONENTS = ["income_tax", "national_insurance"] - tax = add(benunit, period, TAX_COMPONENTS) + tax = add_for_members(benunit, period, TAX_COMPONENTS, members) increased_income_reduced_by_tax_and_pensions = ( increased_income - tax - pension_contributions ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py index 287304c8ad..aa7dd6bd65 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py @@ -7,16 +7,19 @@ class housing_benefit_applicable_income_childcare_element(Variable): label = "Housing Benefit applicable income childcare element" definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/27" def formula(benunit, period, parameters): p = parameters(period).gov.dwp.tax_credits.working_tax_credit.elements - num_children = benunit("benunit_count_children", period) - childcare_amount_1 = (num_children == 1) * p.childcare_1 - childcare_amount_2 = (num_children > 1) * p.childcare_2 + relevant_children = benunit( + "num_relevant_children_for_housing_benefit_childcare", period + ) + childcare_amount_1 = (relevant_children == 1) * p.childcare_1 + childcare_amount_2 = (relevant_children > 1) * p.childcare_2 max_weekly_childcare_amount = childcare_amount_1 + childcare_amount_2 # Assuming that no children leads to no childcare element capped_max_childcare_amount = where( - num_children == 0, 0, max_weekly_childcare_amount + relevant_children == 0, 0, max_weekly_childcare_amount ) max_childcare_amount = capped_max_childcare_amount * WEEKS_IN_YEAR childcare_expenses = add(benunit, period, ["childcare_expenses"]) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 1722ced06b..f2366fcd62 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -7,12 +7,21 @@ class housing_benefit_applicable_income_disregard(Variable): label = "Housing Benefit applicable income disregards" definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2006/213/schedule/4" def formula(benunit, period, parameters): WTC = parameters(period).gov.dwp.tax_credits.working_tax_credit p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard - hours = add(benunit, period, ["weekly_hours"]) - # Calculate single, couple, lone parent, and worker disregards. + # Hours of the claimant and partner and, as the model did before, the + # legacy children or young persons; anyone else in the benefit unit + # does not count (Sch 4 para 17 tests the claimant's or partner's work). + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_young_person_for_legacy_benefits", period + ) + hours = add_for_members(benunit, period, ["weekly_hours"], members) + # The claimant categories are mutually exclusive (Sch 4 paras 4, 7, 10). + # Worker disregard is additional. single = benunit("is_single_person", period) single_disregard = single * p.single couple = benunit("is_couple", period) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py index 50c086675b..e5e84f8d97 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py @@ -8,11 +8,12 @@ class housing_benefit_applicable_amount(Variable): definition_period = YEAR unit = GBP defined_for = "housing_benefit_eligible" + reference = "https://www.legislation.gov.uk/uksi/2006/213/schedule/3" def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) - eldest_age = benunit("eldest_adult_age", period) + eldest_age = benunit("eldest_claimant_or_partner_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index ea49fbf511..8c6b0064dc 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py @@ -7,15 +7,18 @@ class housing_benefit_assessable_capital(Variable): label = "Housing Benefit assessable capital" documentation = ( "Housing Benefit capital counted from the configured capital sources. " - "Household sources are allocated across benunits using a household " - "adult-share proxy; person-level sources, such as a Lifetime ISA, " - "count only for the holder's own benunit, and only when the holder is " - "its claimant or partner (is_uc_claimant): a dependant's capital is " - "not the claimant's." + "Household sources are allocated across benunits in proportion to their " + "claimants and partners, a PolicyEngine convention because household " + "capital data cannot identify ownership; children and young persons add " + "no weight. Person-level sources, such as a Lifetime ISA, count only for " + "the holder's own benunit, and only when the holder is its claimant or " + "partner (is_claimant_or_partner): a dependant's capital is not the " + "claimant's." ) definition_period = YEAR unit = GBP quantity_type = STOCK + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/25" def formula(benunit, period, parameters): household = benunit.household @@ -23,19 +26,23 @@ def formula(benunit, period, parameters): any_over_SP_age = benunit.any(person("is_SP_age", period)) p = parameters(period).gov.dwp.housing_benefit.means_test.capital household_capital = sum(household(source, period) for source in p.sources) - claimant_or_partner = person("is_uc_claimant", period) + claimant_or_partner = person("is_claimant_or_partner", period) person_capital = sum( benunit.sum(person(source, period) * claimant_or_partner) for source in p.person_sources ) - benunit_adults = add(benunit, period, ["is_adult"]) - household_adults = benunit.max( - person.household.sum(person.household.members("is_adult", period)) + benunit_claimants_and_partners = add( + benunit, period, ["is_claimant_or_partner"] ) - adult_divisor = max_(1, household_adults) + household_claimants_and_partners = benunit.max( + person.household.sum(person("is_claimant_or_partner", period)) + ) + claimant_partner_divisor = max_(1, household_claimants_and_partners) household_capital_proxy = where( - household_adults > 0, - household_capital * benunit_adults / adult_divisor, + household_claimants_and_partners > 0, + household_capital + * benunit_claimants_and_partners + / claimant_partner_divisor, 0, ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index 9cab415763..6c65db61b8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -6,8 +6,9 @@ class housing_benefit_eligible(Variable): entity = BenUnit label = "eligible for the Housing Benefit" documentation = ( - "Whether this family can receive Housing Benefit. A family in which " - "every adult has reached the qualifying age for State Pension Credit " + "Whether this family can receive Housing Benefit. A family whose " + "claimant and any partner have reached the qualifying age for State " + "Pension Credit " "can make a new claim; Universal Credit is not available to it. Any " "other family keeps Housing Benefit only while it continues an " "existing award (reported Housing Benefit) and does not claim " @@ -28,15 +29,14 @@ def formula(benunit, period, parameters): sp_age = person("is_SP_age", period) # New claims are barred except where the claimant, and any partner, # has reached the qualifying age for State Pension Credit - # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). Every adult in - # the benefit unit stands in for the claimant and partner, as in - # is_uc_eligible and is_pension_credit_eligible, so a pensioner with - # an 18 or 19 year old dependant is routed to Universal Credit. - # Because is_uc_eligible needs a working-age adult, no family on this - # route receives Universal Credit; change the three together. - adult = person("is_adult", period) - adult_count = benunit.sum(adult) - pension_age = (adult_count > 0) & (benunit.sum(adult & sp_age) == adult_count) + # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). This is the + # same claimant-and-partner set is_uc_eligible and + # is_pension_credit_eligible use, so a pensioner with an 18 or 19 year + # old dependant can claim Housing Benefit and not Universal Credit; + # change the three together. + claimant_or_partner = person("is_claimant_or_partner", period) + count = benunit.sum(claimant_or_partner) + pension_age = (count > 0) & (benunit.sum(claimant_or_partner & sp_age) == count) # Working-age and mixed-age families (since 15 May 2019) claim # Universal Credit instead. They keep an existing award until they # claim it (reg 8(2A)), so this route also rules out receiving diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/num_relevant_children_for_housing_benefit_childcare.py b/policyengine_uk/variables/gov/dwp/housing_benefit/num_relevant_children_for_housing_benefit_childcare.py new file mode 100644 index 0000000000..b678bac9f0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/num_relevant_children_for_housing_benefit_childcare.py @@ -0,0 +1,38 @@ +from policyengine_uk.model_api import * + + +class num_relevant_children_for_housing_benefit_childcare(Variable): + """Children whose ages permit relevant Housing Benefit childcare charges. + + Regulation 28(6) ends eligibility on the day before the first Monday + in September after the fifteenth birthday (sixteenth if disabled). + Annual integer ages approximate this with exclusive limits of 16 and + 17. The legacy-benefit family definition excludes claimants/partners. + Per-child charges are unobserved, so eligible children stand in for + children in respect of whom charges are paid under regulation 27(3). + Disability uses the existing benefits-disability input or blindness; + historic certification and suspended-payment cases are unobserved. + """ + + value_type = int + entity = BenUnit + label = "Relevant children for Housing Benefit childcare charges" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/28", + ) + + def formula(benunit, period, parameters): + person = benunit.members + p = parameters( + period + ).gov.dwp.housing_benefit.means_test.childcare.child_age_limit + child_or_young_person = person( + "is_child_or_young_person_for_legacy_benefits", period + ) + disabled = person("is_disabled_for_benefits", period) | person( + "is_blind", period + ) + age_limit = where(disabled, p.disabled, p.standard) + return benunit.sum(child_or_young_person & (person("age", period) < age_limit)) diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py index 9c8733abf5..a560722817 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py @@ -7,16 +7,19 @@ class income_support_applicable_amount(Variable): label = "Applicable amount of Income Support" definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2" def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support amounts = IS.amounts - younger_age = benunit("youngest_adult_age", period) - older_age = benunit("eldest_adult_age", period) - younger_under_18 = younger_age < 18 - younger_under_25 = younger_age < 25 - older_under_18 = older_age < 18 - has_children = benunit.any(benunit.members("is_child", period)) + younger_age = benunit("youngest_claimant_or_partner_age", period) + older_age = benunit("eldest_claimant_or_partner_age", period) + younger_under_18 = younger_age < amounts.age_threshold.younger + younger_under_25 = younger_age < amounts.age_threshold.older + older_under_18 = older_age < amounts.age_threshold.younger + has_children = benunit( + "is_responsible_for_child_or_young_person_for_legacy_benefits", period + ) single = benunit("is_single", period) single_under_25 = single & ~has_children & younger_under_25 single_over_25 = single & ~has_children & ~younger_under_25 @@ -25,6 +28,10 @@ def formula(benunit, period, parameters): couple_young = ~single & older_under_18 couple_mixed = ~single & ~older_under_18 & younger_under_18 couple_old = ~single & ~younger_under_18 + # Retain the existing simplified couple rates. Schedule 2 para 1(3) + # has higher child/own-right qualification branches (a), (e), and a + # lower rate for an 18-24 claimant with an under-18 partner (f). + # Those distinctions require a separate allowance-policy change. personal_allowance_weekly = select( [ single_under_25, diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index 5f6909cfb8..17aa562d36 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -7,8 +7,17 @@ class income_support_applicable_income(Variable): label = "Relevant income for Income Support means test" definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/8" 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 (IS Regs 1987 reg 23); dropping dependants' + # own income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_young_person_for_legacy_benefits", period + ) IS = parameters(period).gov.dwp.income_support INCOME_COMPONENTS = [ "employment_income", @@ -19,27 +28,24 @@ def formula(benunit, period, parameters): bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: INCOME_COMPONENTS.append("basic_income") - income = add(benunit, period, INCOME_COMPONENTS) - tax = add( - benunit, - period, - ["income_tax", "national_insurance"], + income = add_for_members(benunit, period, INCOME_COMPONENTS, members) + tax = add_for_members( + benunit, period, ["income_tax", "national_insurance"], members ) - income += add(benunit, period, ["social_security_income"]) + income += add_for_members(benunit, period, ["social_security_income"], members) income += benunit("income_support_tariff_income", period) income -= tax - income -= add(benunit, period, ["pension_contributions"]) * 0.5 - family_type = benunit("family_type", period) - families = family_type.possible_values - # Calculate income disregards for each family type. + income -= ( + add_for_members(benunit, period, ["pension_contributions"], members) * 0.5 + ) + # Schedule 8 paras 5, 6 and 10 use mutually exclusive claimant types. mt = IS.means_test - single = family_type == families.SINGLE + single = benunit("is_single_person", period) income_disregard_single = single * mt.income_disregard_single - single = family_type == families.SINGLE income_disregard_couple = ( benunit("is_couple", period) * mt.income_disregard_couple ) - lone_parent = family_type == families.LONE_PARENT + lone_parent = benunit("is_lone_parent", period) income_disregard_lone_parent = lone_parent * mt.income_disregard_lone_parent income_disregard = ( income_disregard_single diff --git a/policyengine_uk/variables/gov/dwp/income_support_assessable_capital.py b/policyengine_uk/variables/gov/dwp/income_support_assessable_capital.py index 07905e8c0e..55c6f2aad1 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/income_support_assessable_capital.py @@ -9,15 +9,18 @@ class income_support_assessable_capital(Variable): "Household capital apportioned to the benefit unit for the Income Support " "capital test. Because the dataset only stores these stocks at household " "level, the model allocates full household capital to any benunit on the " - "IS claim path and only falls back to an adult-share proxy when nobody in " - "the household is on that path. " + "IS claim path and only falls back to a claimant-and-partner share when " + "nobody in the household is on that path. This allocation is a " + "PolicyEngine convention; the data cannot identify ownership of capital. " "Person-level sources, such as a Lifetime ISA, count only for the " "holder's own benunit, and only when the holder is its claimant or " - "partner (is_uc_claimant): a dependant's capital is not the claimant's." + "partner (is_claimant_or_partner): a dependant's capital is not the " + "claimant's." ) definition_period = YEAR unit = GBP quantity_type = STOCK + reference = "https://www.legislation.gov.uk/uksi/1987/1967/regulation/23" def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support @@ -28,29 +31,35 @@ def formula(benunit, period, parameters): # The data model stores these capital stocks at household level. For the # live Income Support path, avoid diluting capital across separate claims: # any benunit on the IS claim path gets the full observed household total. - # If nobody in the household is on that path, fall back to an all-adults - # proxy so direct inspection still returns a usable value. + # If nobody in the household is on that path, use claimant/partner + # weights. Regulation 23 excludes children's and young persons' capital; + # the household allocation itself is a modelling convention. household_capital = sum( benunit.max(person.household(source, period)) for source in sources ) - benunit_adults = add(benunit, period, ["is_adult"]) - household_claiming_adults = benunit.max( + benunit_claimants_and_partners = add( + benunit, period, ["is_claimant_or_partner"] + ) + household_claiming_members = benunit.max( person.household.sum( - person("is_adult", period) & person.benunit("would_claim_IS", period) + person("is_claimant_or_partner", period) + & person.benunit("would_claim_IS", period) ) ) - household_adults = benunit.max( - person.household.sum(person.household.members("is_adult", period)) + household_claimants_and_partners = benunit.max( + person.household.sum(person("is_claimant_or_partner", period)) ) - fallback_divisor = max_(1, household_adults) + fallback_divisor = max_(1, household_claimants_and_partners) claiming_proxy = where(would_claim_is, household_capital, 0) - fallback_proxy = household_capital * benunit_adults / fallback_divisor - claimant_or_partner = person("is_uc_claimant", period) + fallback_proxy = ( + household_capital * benunit_claimants_and_partners / fallback_divisor + ) + claimant_or_partner = person("is_claimant_or_partner", period) person_capital = sum( benunit.sum(person(source, period) * claimant_or_partner) for source in IS.means_test.capital.person_sources ) household_share = where( - household_claiming_adults > 0, claiming_proxy, fallback_proxy + household_claiming_members > 0, claiming_proxy, fallback_proxy ) return household_share + person_capital diff --git a/policyengine_uk/variables/gov/dwp/income_support_eligible.py b/policyengine_uk/variables/gov/dwp/income_support_eligible.py index 0dc992a5bb..c0f6715ffa 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_eligible.py +++ b/policyengine_uk/variables/gov/dwp/income_support_eligible.py @@ -6,10 +6,16 @@ class income_support_eligible(Variable): entity = BenUnit label = "Whether eligible for Income Support" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/1B" def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support - youngest_child_5_or_under = benunit("youngest_child_age", period) <= 5 + # Schedule 1B para 1 says "under 5". Retain the existing inclusive + # comparison pending a separate decision on the annual-age model. + youngest_child_5_or_under = ( + benunit("youngest_child_age_for_legacy_benefits", period) + <= IS.eligibility.lone_parent_youngest_child_age_limit + ) lone_parent = benunit("is_lone_parent", period) lone_parent_with_young_child = lone_parent & youngest_child_5_or_under has_carers = add(benunit, period, ["is_carer_for_benefits"]) > 0 diff --git a/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py b/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py index 9573cc48c6..9152c5078f 100644 --- a/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py +++ b/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py @@ -6,7 +6,10 @@ class is_WTC_eligible(Variable): entity = BenUnit label = "Working Tax Credit eligibility" definition_period = YEAR - reference = "Tax Credits Act 2002 s. 10" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/21/section/10", + "https://www.legislation.gov.uk/uksi/2002/2005/regulation/4", + ) def formula(benunit, period, parameters): WTC = parameters(period).gov.dwp.tax_credits.working_tax_credit @@ -14,15 +17,20 @@ def formula(benunit, period, parameters): person_hours = person("weekly_hours", period) total_hours = benunit.sum(person_hours) max_person_hours = benunit.max(person_hours) - has_disabled_adults = benunit("num_disabled_adults", period) > 0 - family_type = benunit("family_type", period) - families = family_type.possible_values + claimant = person("is_claimant_or_partner", period) + has_disabled_adults = benunit.any( + claimant & person("is_disabled_for_benefits", period) + ) + responsible_for_child = benunit( + "is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit", + period, + ) old = person("age", period.this_year) >= WTC.min_hours.old_age has_old = benunit.any(old) - lone_parent = family_type == families.LONE_PARENT - couple_with_children = family_type == families.COUPLE_WITH_CHILDREN - eldest_25_plus = benunit("eldest_adult_age", period) >= 25 - youngest_under_60 = benunit("youngest_adult_age", period) < 60 + lone_parent = benunit("is_single", period) & responsible_for_child + couple_with_children = benunit("is_couple", period) & responsible_for_child + eldest_25_plus = benunit("eldest_claimant_or_partner_age", period) >= 25 + youngest_under_60 = benunit("youngest_claimant_or_partner_age", period) < 60 # Calculate WTC eligibility group. lower_req = has_disabled_adults | has_old | lone_parent medium_req = couple_with_children & ~lower_req diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py new file mode 100644 index 0000000000..5527820b67 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class is_benefit_cap_single_claimant_rate(Variable): + value_type = bool + entity = BenUnit + label = "Benefit cap single claimant rate applies" + documentation = ( + "Whether the claimant has no partner and is not responsible for a " + "child or young person. The model uses one benefit cap for Universal " + "Credit and Housing Benefit, so the family rate applies if either " + "scheme's child or young-person test is met. Couple status is a proxy " + "for joint-claim status and does not capture Universal Credit couples " + "who may claim as singles." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80A", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75CA", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + ) + + def formula(benunit, period, parameters): + return ~benunit("is_couple", period) & ~benunit( + "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", + period, + ) diff --git a/policyengine_uk/variables/gov/dwp/is_child_for_working_tax_credit_childcare_element.py b/policyengine_uk/variables/gov/dwp/is_child_for_working_tax_credit_childcare_element.py new file mode 100644 index 0000000000..4cdba5da66 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_child_for_working_tax_credit_childcare_element.py @@ -0,0 +1,36 @@ +from policyengine_uk.model_api import * + + +class is_child_for_working_tax_credit_childcare_element(Variable): + """Child whose childcare charges count for the WTC childcare element. + + Relevant childcare charges are for a child the claimant is responsible for + (reg 14(1)). A person is a child until the last day of the week containing + the 1 September after their 15th birthday, or 16th if disabled: receiving + DLA or PIP, or certified blind (reg 14(3)-(4)). Annual ages approximate the + September cut-off. Looked-after children are not a claimant's + responsibility (Child Tax Credit Regulations 2002 reg 3). + """ + + value_type = bool + entity = Person + label = "Child for the Working Tax Credit childcare element" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2002/2005/regulation/14", + "https://www.legislation.gov.uk/uksi/2002/2007/regulation/3", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.dwp.tax_credits.working_tax_credit.childcare_child_age_limit + disabled = person("is_disabled_for_benefits", period) | person( + "is_blind", period + ) + age_limit = where(disabled, p.disabled, p.standard) + return ( + ~person("is_claimant_or_partner", period) + & ~person("is_looked_after_by_local_authority", period) + & (person("age", period) < age_limit) + ) diff --git a/policyengine_uk/variables/gov/dwp/is_child_or_young_person_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/is_child_or_young_person_for_legacy_benefits.py new file mode 100644 index 0000000000..7091a57918 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_child_or_young_person_for_legacy_benefits.py @@ -0,0 +1,38 @@ +from policyengine_uk.model_api import * + + +class is_child_or_young_person_for_legacy_benefits(Variable): + """Child or young person in the family for the legacy means-tested schemes. + + Income Support, income-based Jobseeker's Allowance, income-related + Employment and Support Allowance, Housing Benefit and council tax + reduction schemes define a child as a person under 16 (SSCBA 1992 + s.137(1)) and a young person as a Child Benefit qualifying young person + (SSCBA 1992 s.142), excluding young persons entitled to a benefit in + their own right. That is the Child Benefit child or qualifying young + person test, restricted to members who are not the claimant or partner. + A child or young person placed with the claimant by a local authority is + not a member of the claimant's household (IS reg 16(4); HB reg 21(3)). + """ + + value_type = bool + entity = Person + label = "Child or young person for legacy means-tested benefits" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/137", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/14", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/76", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/16", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/21", + ) + + def formula(person, period, parameters): + return ( + person("is_child_or_qualifying_young_person_for_child_benefit", period) + & ~person("is_claimant_or_partner", period) + & ~person("is_looked_after_by_local_authority", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py new file mode 100644 index 0000000000..2e43025fa3 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py @@ -0,0 +1,52 @@ +from policyengine_uk.model_api import * + + +class is_lha_shared_accommodation_rate_specified_renter(Variable): + value_type = bool + entity = BenUnit + label = "LHA renter restricted to the shared accommodation rate" + documentation = ( + "The modelled specified-renter conditions: no partner, below the shared " + "accommodation age threshold, not responsible for a child or young " + "person under the UC or Housing Benefit rules, no non-dependant under " + "the household composition proxy, and not excepted by a disability " + "benefit (UC Schedule 4 paragraph 29(5)). Other paragraph 29 " + "exceptions and couples claiming as single people are not modelled. " + "The same category serves Housing Benefit, whose young-individual " + "definition (HB regulation 2(1)) has no disability exception." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/27", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/29", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + ) + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.LHA + person = benunit.members + # UC Sch 4 para 29(5): a renter under 35 receiving + # attendance allowance, DLA care at the middle or highest rate, or the + # PIP daily living component is excepted. Other para 29 exceptions + # (care leavers, hostel residents, MAPPA, domestic abuse, modern + # slavery, foster parents) are not observed. + excepted_disabled_renter = benunit.any( + person("is_claimant_or_partner", period) + & (add(person, period, p.shared_accommodation_exception_benefits) > 0) + ) + return ( + ~benunit("is_couple", period) + & ( + benunit("eldest_claimant_or_partner_age", period) + < p.shared_accommodation_age_threshold + ) + & ~benunit( + "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", + period, + ) + & ~benunit("lha_renter_has_non_dependant", period) + & ~excepted_disabled_renter + ) diff --git a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit.py b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit.py new file mode 100644 index 0000000000..17c5eaa8e4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit( + Variable +): + value_type = bool + entity = BenUnit + label = "Responsible for a child or qualifying young person for tax credits" + documentation = ( + "Whether the claimant (or either joint claimant) is responsible for " + "at least one child or qualifying young person under the Child Tax " + "Credit rules, which Working Tax Credit also applies. Benefit-unit " + "membership stands in for 'normally living with'." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/21/section/8", + "https://www.legislation.gov.uk/uksi/2002/2007/regulation/3", + "https://www.legislation.gov.uk/uksi/2002/2005/regulation/2", + ) + + def formula(benunit, period, parameters): + person = benunit.members + return benunit.any( + person("is_child_or_qualifying_young_person_for_child_tax_credit", period) + & ~person("is_claimant_or_partner", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_legacy_benefits.py new file mode 100644 index 0000000000..79309c9df7 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_legacy_benefits.py @@ -0,0 +1,21 @@ +from policyengine_uk.model_api import * + + +class is_responsible_for_child_or_young_person_for_legacy_benefits(Variable): + value_type = bool + entity = BenUnit + label = "Responsible for a child or young person for legacy means-tested benefits" + documentation = ( + "Whether the claimant or partner is responsible for a child or young " + "person in the same household, using benefit-unit membership." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + ) + + def formula(benunit, period, parameters): + return benunit.any( + benunit.members("is_child_or_young_person_for_legacy_benefits", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py new file mode 100644 index 0000000000..2897b52f3a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py @@ -0,0 +1,43 @@ +from policyengine_uk.model_api import * + + +class is_responsible_for_child_or_young_person_for_uc_or_housing_benefit(Variable): + """Responsible for a child or young person under UC or Housing Benefit rules. + + The benefit cap and the LHA category each serve both Universal Credit and + Housing Benefit in this model. UC counts a child or qualifying young person + (UC Regs 2013 regs 4-5); Housing Benefit counts a child or young person, + whose young-person test follows Child Benefit (HB Regs 2006 regs 2, 19). + The family rates apply if either scheme's test is met, so neither scheme's + families are moved onto the single rate by the other's narrower definition. + + UC reg 5(1)(a) also makes every 16-year-old a qualifying young person until + the 1 September after their 16th birthday, whatever their education. Annual + ages cannot place that date, so a 16-year-old in the benefit unit who is not + a claimant or partner counts here; the UC child element does not yet model + reg 5(1)(a). + """ + + value_type = bool + entity = BenUnit + label = "Responsible for a child or young person (UC or Housing Benefit)" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + ) + + def formula(benunit, period, parameters): + person = benunit.members + age = person("age", period) + sixteen = (age >= 16) & (age < 17) & ~person("is_claimant_or_partner", period) + return ( + benunit( + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + period, + ) + | benunit( + "is_responsible_for_child_or_young_person_for_legacy_benefits", period + ) + | benunit.any(sixteen) + ) diff --git a/policyengine_uk/variables/gov/dwp/jsa_income_assessable_capital.py b/policyengine_uk/variables/gov/dwp/jsa_income_assessable_capital.py index 3fa403e268..ec3a6631ee 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_assessable_capital.py @@ -10,15 +10,18 @@ class jsa_income_assessable_capital(Variable): "JSA capital test. Because the dataset only stores these stocks at " "household level, the model allocates full household capital to any " "benunit with a reported income-based JSA award and only falls back to " - "an adult-share proxy when nobody in the household is on that reported " - "claim path. " + "a claimant-and-partner share when nobody in the household is on that " + "reported claim path. This allocation is a PolicyEngine convention; " + "the data cannot identify ownership of capital. " "Person-level sources, such as a Lifetime ISA, count only for the " "holder's own benunit, and only when the holder is its claimant or " - "partner (is_uc_claimant): a dependant's capital is not the claimant's." + "partner (is_claimant_or_partner): a dependant's capital is not the " + "claimant's." ) definition_period = YEAR unit = GBP quantity_type = STOCK + reference = "https://www.legislation.gov.uk/uksi/1996/207/regulation/88" def formula(benunit, period, parameters): JSA = parameters(period).gov.dwp.JSA.income @@ -29,19 +32,23 @@ def formula(benunit, period, parameters): household_capital = sum( benunit.max(person.household(source, period)) for source in sources ) - benunit_adults = add(benunit, period, ["is_adult"]) + # Regulation 88(2) excludes children's and young persons' capital. + # The claimant/partner weights approximate otherwise unobserved ownership. + benunit_claimants_and_partners = add( + benunit, period, ["is_claimant_or_partner"] + ) household_reporting_claimants = benunit.max( - person.household.sum( - person("is_adult", period) & (person("jsa_income_reported", period) > 0) - ) + person.household.sum(person("jsa_income_reported", period) > 0) ) - household_adults = benunit.max( - person.household.sum(person.household.members("is_adult", period)) + household_claimants_and_partners = benunit.max( + person.household.sum(person("is_claimant_or_partner", period)) ) - fallback_divisor = max_(1, household_adults) + fallback_divisor = max_(1, household_claimants_and_partners) claiming_proxy = where(claiming_jsa_income, household_capital, 0) - fallback_proxy = household_capital * benunit_adults / fallback_divisor - claimant_or_partner = person("is_uc_claimant", period) + fallback_proxy = ( + household_capital * benunit_claimants_and_partners / fallback_divisor + ) + claimant_or_partner = person("is_claimant_or_partner", period) person_capital = sum( benunit.sum(person(source, period) * claimant_or_partner) for source in JSA.capital.person_sources diff --git a/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py new file mode 100644 index 0000000000..347788ab60 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/lha_renter_has_non_dependant.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * + + +class lha_renter_has_non_dependant(Variable): + value_type = bool + entity = BenUnit + label = "LHA renter has a non-dependant (household composition proxy)" + documentation = ( + "A conservative proxy: a claimant or partner of another benefit unit " + "in the household, or someone in this benefit unit who is neither a " + "claimant/partner nor a UC child or qualifying young person. The model " + "does not identify the statutory exclusions for joint renters, " + "commercial lodgers, landlords or foster children here. Rent liability " + "alone does not exclude another benefit unit's claimant." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + ) + + def formula(benunit, period, parameters): + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + # A child or young person under either scheme (UC reg 5; HB reg 19) + # is a dependant, not a non-dependant. + age = person("age", period) + child_or_qyp = ( + person("is_child_or_qualifying_young_person_for_universal_credit", period) + | person("is_child_or_young_person_for_legacy_benefits", period) + | ((age >= 16) & (age < 17)) + ) + within_benefit_unit = benunit.any(~claimant_or_partner & ~child_or_qyp) + other_benefit_unit_claimants = benunit.max( + person.household.sum(claimant_or_partner) + ) - benunit.sum(claimant_or_partner) + return within_benefit_unit | (other_benefit_unit_claimants > 0) diff --git a/policyengine_uk/variables/gov/dwp/num_disabled_claimants_or_partners_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/num_disabled_claimants_or_partners_for_legacy_benefits.py new file mode 100644 index 0000000000..4635524208 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/num_disabled_claimants_or_partners_for_legacy_benefits.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class num_disabled_claimants_or_partners_for_legacy_benefits(Variable): + value_type = int + entity = BenUnit + label = "Disabled claimants or partners for legacy benefits" + documentation = ( + "Claimants and partners satisfying the model's existing " + "is_disabled_for_benefits indicator. Children and young persons " + "do not qualify a claimant for this premium through that indicator." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/11", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/12", + ) + + def formula(benunit, period, parameters): + claimant_or_partner = benunit.members("is_claimant_or_partner", period) + disabled = benunit.members("is_disabled_for_benefits", period) + return benunit.sum(claimant_or_partner & disabled) diff --git a/policyengine_uk/variables/gov/dwp/num_enhanced_disabled_claimants_or_partners_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/num_enhanced_disabled_claimants_or_partners_for_legacy_benefits.py new file mode 100644 index 0000000000..6b7428f74e --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/num_enhanced_disabled_claimants_or_partners_for_legacy_benefits.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class num_enhanced_disabled_claimants_or_partners_for_legacy_benefits(Variable): + value_type = int + entity = BenUnit + label = "Enhanced-disabled claimants or partners for legacy benefits" + documentation = ( + "Claimants and partners satisfying the model's existing " + "is_enhanced_disabled_for_benefits indicator. This count excludes " + "children and young persons, whose separate enhanced premium is " + "not modelled here." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/13A", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/15", + ) + + def formula(benunit, period, parameters): + claimant_or_partner = benunit.members("is_claimant_or_partner", period) + disabled = benunit.members("is_enhanced_disabled_for_benefits", period) + return benunit.sum(claimant_or_partner & disabled) diff --git a/policyengine_uk/variables/gov/dwp/num_severely_disabled_claimants_or_partners_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/num_severely_disabled_claimants_or_partners_for_legacy_benefits.py new file mode 100644 index 0000000000..2d6f4987ad --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/num_severely_disabled_claimants_or_partners_for_legacy_benefits.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class num_severely_disabled_claimants_or_partners_for_legacy_benefits(Variable): + value_type = int + entity = BenUnit + label = "Severely disabled claimants or partners for legacy benefits" + documentation = ( + "Claimants and partners satisfying the model's existing " + "is_severely_disabled_for_benefits indicator. This count does not " + "implement the separate statutory non-dependant or carer conditions." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/13", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14", + ) + + def formula(benunit, period, parameters): + claimant_or_partner = benunit.members("is_claimant_or_partner", period) + disabled = benunit.members("is_severely_disabled_for_benefits", period) + return benunit.sum(claimant_or_partner & disabled) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py index 2eb5599e97..6f2c714ac5 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py @@ -7,7 +7,18 @@ class guarantee_credit(Variable): definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/ukpga/2002/16/section/2" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2002/16/section/2", + ) + documentation = ( + "The guarantee credit element of State Pension Credit. It is part of " + "State Pension Credit, so it is zero unless the benefit unit meets " + "the Pension Credit entitlement conditions (including State Pension " + "age for the claimant and any partner); other schemes that passport " + "on Guarantee Credit read this variable." + ) + defined_for = "is_pension_credit_eligible" def formula(benunit, period, parameters): income = benunit("pension_credit_income", period) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/carer_minimum_guarantee_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/carer_minimum_guarantee_addition.py index e500ec8f78..b7c4b625d5 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/carer_minimum_guarantee_addition.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/carer_minimum_guarantee_addition.py @@ -10,7 +10,7 @@ class carer_minimum_guarantee_addition(Variable): reference = "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/4" documentation = ( - "Paid for each claimant entitled to Carer's Allowance, which includes " + "Paid for each claimant or partner entitled to Carer's Allowance, which includes " "underlying entitlement where the allowance is not paid (for example " "under the overlapping-benefit rule), so the test is is_carer_for_benefits " "rather than receipt." @@ -18,6 +18,7 @@ class carer_minimum_guarantee_addition(Variable): def formula(benunit, period, parameters): is_carer = benunit.members("is_carer_for_benefits", period) - num_carers = benunit.sum(is_carer) + is_claimant_or_partner = benunit.members("is_claimant_or_partner", period) + num_carers = benunit.sum(is_carer & is_claimant_or_partner) gc = parameters(period).gov.dwp.pension_credit.guarantee_credit return num_carers * gc.carer.addition * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py index 91af615384..8a6677bece 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/child_minimum_guarantee_addition.py @@ -11,14 +11,14 @@ class child_minimum_guarantee_addition(Variable): def formula(benunit, period, parameters): person = benunit.members - is_child = person( + is_child_or_qualifying_young_person = person( "is_child_or_qualifying_young_person_for_pension_credit", period ) child_index = ( person.get_rank( person.benunit, -person("age", period), - condition=is_child, + condition=is_child_or_qualifying_young_person, ) + 1 ) @@ -40,9 +40,9 @@ def formula(benunit, period, parameters): per_child_amount = ( select( [ - is_child & is_not_disabled, - is_child & is_standard_disabled, - is_child & is_severely_disabled, + is_child_or_qualifying_young_person & is_not_disabled, + is_child_or_qualifying_young_person & is_standard_disabled, + is_child_or_qualifying_young_person & is_severely_disabled, ], [ child_addition, diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py index 2a95fd2cae..212c027744 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py @@ -7,34 +7,52 @@ class severe_disability_minimum_guarantee_addition(Variable): definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1" + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/6", + ) def formula(benunit, period, parameters): - # 1. At least one adult receives a qualifying benefit - # 2. No children (except children receiving qualifying benefits) - # 3. Nobody receives Carer's Allowance (technically 'for one of the - # claimants', but we assume this is true). This is receipt, not - # entitlement: the addition is withheld only where a carer's - # allowance is "entitled to and in receipt of" for the claimant, so - # a carer with underlying entitlement alone does not block it. + # Count qualifying claimants/partners; children and qualifying young + # people are ignored under Sch I para 2(2)(f). + # Sch I para 1(1): a single claimant qualifies if no carer benefit is + # paid for them (a). A couple qualifies if both partners receive a + # qualifying benefit and a carer benefit is paid for at most one of + # them (b), or if one does, the other is blind and no carer benefit is + # paid for the first (c). Reg 6(5): the double amount applies under (b) + # when no carer benefit is paid for either partner, otherwise the + # single amount. + # Retained simplifications: the household-level non-dependant + # residence test is not modelled; the person cared for is not + # identified, so each carer benefit paid in the benefit unit is taken + # to be for a different claimant or partner, never for its recipient + # (so a qualifying claimant's own carer benefit does not bar them); + # a UC carer element paid to someone outside the benefit unit is not + # seen; underlying entitlement alone does not count. severe_disability = parameters( period ).gov.dwp.pension_credit.guarantee_credit.severe_disability - relevant_benefits = severe_disability.relevant_benefits person = benunit.members - person_receives_qualifying_benefits = add(person, period, relevant_benefits) > 0 - is_adult = person("is_adult", period) - count_eligible_adults = benunit.sum( - is_adult & person_receives_qualifying_benefits + claimant_or_partner = person("is_claimant_or_partner", period) + qualifies = add(person, period, severe_disability.relevant_benefits) > 0 + qualifier = claimant_or_partner & qualifies + qualifying = benunit.sum(qualifier) + other_partner_blind = benunit.any( + claimant_or_partner & ~qualifies & person("is_blind", period) ) - any_children_without_benefits = ( - benunit.sum(~is_adult & ~person_receives_qualifying_benefits) > 0 - ) - carer_benefit_received = add(benunit, period, ["receives_carer_benefit"]) > 0 - eligible = ~any_children_without_benefits & ~carer_benefit_received - return ( - eligible - * count_eligible_adults - * severe_disability.addition - * WEEKS_IN_YEAR + receives_carer_benefit = person("receives_carer_benefit", period) + carers = benunit.sum(receives_carer_benefit) + # Heads (a) and (c): carer benefits paid to anyone but the qualifying + # claimant or partner. + carers_for_qualifier = benunit.sum(receives_carer_benefit & ~qualifier) + amounts = where( + benunit("is_couple", period), + where( + qualifying >= 2, + max_(2 - carers, 0), + (qualifying == 1) & other_partner_blind & (carers_for_qualifier == 0), + ), + (qualifying >= 1) & (carers_for_qualifier == 0), ) + return amounts * severe_disability.addition * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py index d3c12d4372..ec51831654 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py @@ -7,18 +7,23 @@ class is_pension_credit_eligible(Variable): definition_period = YEAR value_type = bool unit = GBP - reference = "https://www.legislation.gov.uk/ukpga/2002/16/section/1" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2002/16/section/4", + ) def formula(benunit, period, parameters): - adult = benunit.members("is_adult", period) - adult_count = benunit.sum(adult) - all_adults_are_sp_age = ( - benunit.sum(adult & benunit.members("is_SP_age", period)) == adult_count + claimant_or_partner = benunit.members("is_claimant_or_partner", period) + claimant_count = benunit.sum(claimant_or_partner) + all_claimants_are_sp_age = ( + benunit.sum(claimant_or_partner & benunit.members("is_SP_age", period)) + == claimant_count ) + # Mixed-age transitional protection under SI 2019/37 art. 4 is not modelled. is_gc_eligible = benunit("is_guarantee_credit_eligible", period) is_sc_eligible = benunit("is_savings_credit_eligible", period) return ( - (adult_count > 0) - & all_adults_are_sp_age + (claimant_count > 0) + & all_claimants_are_sp_age & (is_gc_eligible | is_sc_eligible) ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings.py index 9cf8040c3b..c4a6137b6a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings.py @@ -3,9 +3,24 @@ class pension_credit_earnings(Variable): label = "earnings for Pension Credit" + documentation = ( + "Earnings of the claimant and partner. A claimant's income includes " + "their partner's (State Pension Credit Act 2002 s.5), but not that of " + "children or young persons in the family." + ) entity = BenUnit definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17A" - adds = "gov.dwp.pension_credit.guarantee_credit.earnings_sources" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/5", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17A", + ) + + def formula(benunit, period, parameters): + sources = parameters( + period + ).gov.dwp.pension_credit.guarantee_credit.earnings_sources + person = benunit.members + earnings = add(person, period, sources) + return benunit.sum(earnings * person("is_claimant_or_partner", period)) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index df44d712f8..1160800f3a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -7,17 +7,43 @@ class pension_credit_income(Variable): definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/ukpga/2002/16/section/15" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/5", + "https://www.legislation.gov.uk/ukpga/2002/16/section/15", + ) + documentation = ( + "Income and deductions of the claimant and partner, together with " + "benefit-unit income sources. Dependants' income and deductions " + "are excluded." + ) def formula(benunit, period, parameters): pc = parameters(period).gov.dwp.pension_credit sources = pc.guarantee_credit.income - total = add(benunit, period, sources) + person = benunit.members + is_claimant_or_partner = person("is_claimant_or_partner", period) + total = 0 + for source in sources: + if person.entity.get_variable(source).entity.is_person: + total += benunit.sum(person(source, period) * is_claimant_or_partner) + else: + total += add(benunit, period, [source]) + + # Benefit-unit sources (pension_credit_earnings, which already counts + # only the claimant and partner, working tax credit and tariff income) + # are counted once. bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: - total += add(benunit, period, ["basic_income"]) - pension_contributions = add(benunit, period, ["pension_contributions"]) - tax = add(benunit, period, ["income_tax", "national_insurance"]) + total += benunit.sum( + person("basic_income", period) * is_claimant_or_partner + ) + pension_contributions = benunit.sum( + person("pension_contributions", period) * is_claimant_or_partner + ) + tax = benunit.sum( + add(person, period, ["income_tax", "national_insurance"]) + * is_claimant_or_partner + ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate return max_(0, total - deductions) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.py index 85ac297559..a7263e8b74 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.py @@ -10,9 +10,10 @@ class is_savings_credit_eligible(Variable): reference = "https://www.legislation.gov.uk/ukpga/2002/16/section/3" def formula(benunit, period, parameters): - # Check if any member reached SPA before the cutoff year + # The claimant or partner must have reached SPA before the cutoff year. has_pre_cutoff_spa_member = benunit.any( - benunit.members("meets_savings_credit_age_requirement", period) + benunit.members("is_claimant_or_partner", period) + & benunit.members("meets_savings_credit_age_requirement", period) ) income = benunit("savings_credit_income", period) sc = parameters(period).gov.dwp.pension_credit.savings_credit diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py index f62e21e744..856e7e99f1 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py @@ -3,15 +3,26 @@ class savings_credit_income(Variable): label = "Income for Savings Credit" - documentation = "Savings Credit (Pension Credit) excludes certain income sources from the calculation of the amount." + documentation = ( + "Savings Credit (Pension Credit) excludes certain income sources of " + "the claimant and partner from the calculation of the amount." + ) entity = BenUnit definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/uksi/2002/1792/regulation/9" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/5", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/9", + ) def formula(benunit, period, parameters): guarantee_credit_income = benunit("pension_credit_income", period) pc = parameters(period).gov.dwp.pension_credit - excluded_income = add(benunit, period, pc.savings_credit.excluded_income) + person = benunit.members + is_claimant_or_partner = person("is_claimant_or_partner", period) + excluded_income = benunit.sum( + add(person, period, pc.savings_credit.excluded_income) + * is_claimant_or_partner + ) return max_(0, guarantee_credit_income - excluded_income) diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py index d2d67ed018..1c698c1921 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -12,6 +12,14 @@ class tax_credits_applicable_income(Variable): ) 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 (TCA 2002 s.7); dropping dependants' own + # income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_qualifying_young_person_for_child_tax_credit", period + ) TC = parameters(period).gov.dwp.tax_credits STEP_1_COMPONENTS = [ "private_pension_income", @@ -19,7 +27,7 @@ def formula(benunit, period, parameters): "dividend_income", "property_income", ] - income = add(benunit, period, STEP_1_COMPONENTS) + income = add_for_members(benunit, period, STEP_1_COMPONENTS, members) income = max_(income - TC.means_test.non_earned_disregard, 0) STEP_2_COMPONENTS = [ "employment_income", @@ -30,7 +38,9 @@ def formula(benunit, period, parameters): bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: STEP_2_COMPONENTS.append("basic_income") - income += add(benunit, period, STEP_2_COMPONENTS) + income += add_for_members(benunit, period, STEP_2_COMPONENTS, members) EXEMPT_BENEFITS = ["income_support", "esa_income", "jsa_income"] - on_exempt_benefits = add(benunit, period, EXEMPT_BENEFITS) > 0 + on_exempt_benefits = ( + add_for_members(benunit, period, EXEMPT_BENEFITS, members) > 0 + ) return income * ~on_exempt_benefits diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_individual_child_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_individual_child_element.py index a253eeabc1..bca4fe80d0 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_individual_child_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/uc_individual_child_element.py @@ -11,10 +11,10 @@ class uc_individual_child_element(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.child - child_index = person("uc_child_index", period) + uc_child_index = person("uc_child_index", period) born_before_limit = person("uc_is_child_born_before_child_limit", period) child_limit_applying = where(~born_before_limit, p.limit.child_count, inf) - is_eligible = (child_index != -1) & (child_index <= child_limit_applying) + is_eligible = (uc_child_index != -1) & (uc_child_index <= child_limit_applying) # Reform proposal age_exemption = ( @@ -29,7 +29,7 @@ def formula(person, period, parameters): return ( select( [ - (child_index == 1) & born_before_limit & is_eligible, + (uc_child_index == 1) & born_before_limit & is_eligible, is_eligible, ], [ diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py index b1a0765b15..661786c65f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py @@ -5,14 +5,17 @@ class uc_childcare_work_condition(Variable): value_type = bool entity = BenUnit label = "Meets Universal Credit childcare work condition" + documentation = ( + "Tests work for the claimant and partner, excluding dependants. " + "The in_work proxy means positive hours or earnings; offers of work, " + "the partner exceptions in regulation 32(1)(b), and the treated-as-working " + "rules in regulation 32(2) are not modelled." + ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32/2020-04-06" + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32" def formula(benunit, period, parameters): person = benunit.members - adult = person("is_adult", period) + claimant = person("is_uc_claimant", period) in_work = person("in_work", period) - adults_in_work = adult & in_work - # Benefit unit must not have any adults not in work. - all_adults_in_work = benunit.all(in_work | ~adult) - return benunit.any(adults_in_work) & all_adults_in_work + return benunit.any(claimant & in_work) & benunit.all(in_work | ~claimant) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_adults.py b/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_adults.py deleted file mode 100644 index 5b080f916e..0000000000 --- a/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_adults.py +++ /dev/null @@ -1,21 +0,0 @@ -from policyengine_uk.model_api import * - - -class household_uc_unreported_adults(Variable): - value_type = int - entity = Household - label = "Number of adults in benunits without reported UC capital" - documentation = ( - "Adults counted in the residual household-capital proxy for Universal " - "Credit, including adults in non-claimant benunits when capital " - "ownership cannot be identified." - ) - definition_period = YEAR - unit = "adult" - - def formula(household, period, parameters): - person = household.members - reported_capital = person.benunit("uc_reported_capital", period) - has_reported_capital = reported_capital >= 0 - is_adult = person("is_adult", period) - return household.sum(is_adult * ~has_reported_capital) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_claimants.py b/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_claimants.py new file mode 100644 index 0000000000..66786f1877 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/household_uc_unreported_claimants.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class household_uc_unreported_claimants(Variable): + value_type = int + entity = Household + label = "Number of claimants and partners in benunits without reported UC capital" + documentation = ( + "Claimants and partners counted in PolicyEngine's allocation of residual " + "household capital for Universal Credit. This includes claimant and " + "partner roles in benefit units that do not receive Universal Credit. " + "Allocating unidentified household capital by these counts is a " + "modelling convention; legislation defines whose capital is assessed, " + "not how household-level data should be allocated." + ) + definition_period = YEAR + unit = "person" + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/5", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/18", + ) + + def formula(household, period, parameters): + person = household.members + reported_capital = person.benunit("uc_reported_capital", period) + has_reported_capital = reported_capital >= 0 + is_uc_claimant = person("is_uc_claimant", period) + return household.sum(is_uc_claimant * ~has_reported_capital) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py index 8ff40e4a84..6e496b8681 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py @@ -8,13 +8,24 @@ class uc_earned_income(Variable): definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/22" + def formula(benunit, period, parameters): - personal_gross_earned_income = add( - benunit, period, ["uc_mif_capped_earned_income"] + # 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 (UC Regs 2013 reg 22); dropping dependants' + # own income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) + personal_gross_earned_income = add_for_members( + benunit, period, ["uc_mif_capped_earned_income"], members ) - disregards = add( + disregards = add_for_members( benunit, period, - ["uc_work_allowance", "benunit_tax", "pension_contributions"], + ["uc_work_allowance", "tax", "pension_contributions"], + members, ) return max_(0, personal_gross_earned_income - disregards) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py index 8dc07ee660..82b6ec328b 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py @@ -9,9 +9,17 @@ class uc_unearned_income(Variable): unit = GBP 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 (UC Regs 2013 reg 22); dropping dependants' + # own income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) p = parameters(period).gov.dwp.universal_credit.means_test household = benunit.household - total = add(benunit, period, p.income_definitions.unearned) + total = add_for_members(benunit, period, p.income_definitions.unearned, members) tariff_income_applies = benunit("uc_tariff_income", period) > 0 reported_capital = benunit("uc_reported_capital", period) has_reported_capital = reported_capital >= 0 @@ -20,10 +28,10 @@ def formula(benunit, period, parameters): ) + household("non_residential_property_value", period) capital_derived_income = ( ((household("savings", period) > 0) | has_reported_capital) - * benunit("savings_interest_income", period) + * add_for_members(benunit, period, ["savings_interest_income"], members) + ((household("corporate_wealth", period) > 0) | has_reported_capital) - * benunit("dividend_income", period) + * add_for_members(benunit, period, ["dividend_income"], members) + ((property_capital > 0) | has_reported_capital) - * benunit("property_income", period) + * add_for_members(benunit, period, ["property_income"], members) ) return total - tariff_income_applies * capital_derived_income diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_responsible_for_child_or_qualifying_young_person_for_universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_responsible_for_child_or_qualifying_young_person_for_universal_credit.py new file mode 100644 index 0000000000..339a3eb57b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_responsible_for_child_or_qualifying_young_person_for_universal_credit.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class is_responsible_for_child_or_qualifying_young_person_for_universal_credit( + Variable +): + value_type = bool + entity = BenUnit + label = "Responsible for a child or qualifying young person for Universal Credit" + documentation = ( + "Whether the claimant (or either joint claimant) is responsible for " + "at least one child (under 16) or qualifying young person, using " + "benefit-unit membership for 'normally lives with'." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/40", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + ) + + def formula(benunit, period, parameters): + person = benunit.members + return benunit.any( + person("is_child_or_qualifying_young_person_for_universal_credit", period) + & ~person("is_uc_claimant", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py index 4e758a7e7e..2033c754fb 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py @@ -2,17 +2,13 @@ class is_uc_claimant(Variable): - """Claimant or partner, rather than a dependent member of the benefit unit. + """Claimant or partner, rather than a child or qualifying young person. - Datasets should supply this from recorded relationships. The calculator - fallback includes the family head, identified parents, and adults, except - UC qualifying young people in a benefit unit with an identified parent. - Education defaults to tertiary education at ages 18 and 19, so treating - those ages as dependants requires qualifying education and any applicable - entry and terminal-date inputs. A partner in qualifying education who is - not identified as a head or parent, or a dependent adult outside the UC - child definition, needs an explicit input when relationships remain - ambiguous. This flag does not establish eligibility. + A Universal Credit claim is made by a single claimant or by the members of + a couple jointly; children and qualifying young persons are people a + claimant is responsible for. This is the benefit unit's claimant or + partner (`is_claimant_or_partner`). It does not establish eligibility: + the UC age and other basic conditions are applied separately. """ value_type = bool @@ -21,17 +17,9 @@ class is_uc_claimant(Variable): definition_period = YEAR reference = ( "https://www.legislation.gov.uk/ukpga/2012/5/section/39", + "https://www.legislation.gov.uk/ukpga/2012/5/section/40", "https://www.legislation.gov.uk/uksi/2013/376/regulation/3/2", - "https://www.gov.uk/universal-credit/eligibility", ) def formula(person, period, parameters): - has_parent = add(person.benunit, period, ["is_parent"]) > 0 - qualifying_child = person( - "is_child_or_qualifying_young_person_for_universal_credit", period - ) - return ( - person("is_benunit_head", period) - | person("is_parent", period) - | (person("is_adult", period) & ~(qualifying_child & has_parent)) - ) + return person("is_claimant_or_partner", period) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py index 2dbd88a4f9..f8c205416b 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py @@ -5,11 +5,26 @@ class is_uc_eligible(Variable): value_type = bool entity = BenUnit label = "Eligible for the Universal Credit" - documentation = "Whether this family is eligible for Universal Credit" + documentation = ( + "Whether a claimant or partner meets the modelled minimum-age and " + "pension-age conditions and the benefit unit meets the capital limit. " + "One qualifying claimant suffices, preserving mixed-age couples under " + "regulation 3(2)(a). Dependants cannot satisfy the claimant age test." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/3", + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ) def formula(benunit, period, parameters): capital = benunit("uc_assessable_capital", period) limit = parameters(period).gov.dwp.universal_credit.means_test.capital.limit - has_working_age_adult = benunit.any(benunit.members("is_WA_adult", period)) - return has_working_age_adult & (capital <= limit) + claimant = benunit.members("is_uc_claimant", period) + meets_minimum_age = benunit.members("meets_uc_minimum_age_condition", period) + pension_age = benunit.members("is_SP_age", period) + has_qualifying_claimant = benunit.any( + claimant & meets_minimum_age & ~pension_age + ) + return has_qualifying_claimant & (capital <= limit) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_minimum_age_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_minimum_age_condition.py new file mode 100644 index 0000000000..cd1ad10c7f --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_minimum_age_condition.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * + + +class meets_uc_minimum_age_condition(Variable): + value_type = bool + entity = Person + label = "Meets the Universal Credit minimum age condition" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/8", + ) + documentation = """ + The standard minimum age is 18; regulation 8 permits claims from age 16 + in specified circumstances. Observable proxies cover limited capability + for work, caring, and responsibility for a child. + + uc_limited_capability_for_WRA is the existing LCWRA-named proxy (it defaults + to is_disabled_for_benefits), not an observation of LCW or a pending + assessment supported by medical evidence under regulation 8(1)(a)-(b). + is_carer_for_benefits means receipt of Carer's Allowance or Scottish Carer + Support Payment, or at least the Carer's Allowance qualifying hours of + care (currently 35 weekly). Its hours limb does not verify the recipient's + qualifying disability benefit, and it does not exclude paid care. + + For regulation 8(1)(d), responsibility is proxied by being a claimant in a + benefit unit with a nonclaimant child under the UC child definition. A + qualifying young person alone does not satisfy this limb. Benefit-unit + membership does not observe individual legal responsibility. + + Regulation 8(1)(e)-(g) (a partner's responsibility, pregnancy/recent birth, + and absence of parental support) are not observed. The regulation 8(2) + care-leaver exclusion, which applies to (c), (f), and (g), is also not + observed. These provisions are not separately modelled. + """ + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.universal_credit.eligibility + age = person("age", period) + claimant = person("is_uc_claimant", period) + child = person("is_child_for_universal_credit", period) + responsible_for_child = claimant & person.benunit.any(child & ~claimant) + reduced_age_exception = ( + person("uc_limited_capability_for_WRA", period) + | person("is_carer_for_benefits", period) + | responsible_for_child + ) + return (age >= p.min_age) | ((age >= p.reduced_min_age) & reduced_age_exception) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_assessable_capital.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_assessable_capital.py index ebe2537056..292789c1d2 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/uc_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_assessable_capital.py @@ -7,15 +7,24 @@ class uc_assessable_capital(Variable): label = "Universal Credit assessable capital" documentation = ( "Universal Credit capital counted from the configured capital sources, " - "with benunit-reported overrides when available. Household sources are " - "allocated by the household proxy; person-level sources, such as a " - "Lifetime ISA, count only for the holder's own benunit, and only when " - "the holder is its claimant or partner (is_uc_claimant): a dependant's " - "capital is not the claimant's." + "with benunit-reported overrides when available. PolicyEngine allocates " + "the remaining household capital between unreported benefit units in " + "proportion to their claimant and partner counts, including units that " + "do not receive Universal Credit. This allocation is a modelling " + "convention when capital ownership is unobserved, not a statutory rule. " + "The claimant and partner owner set follows section 5 of the Welfare " + "Reform Act 2012 and regulation 18 of the Universal Credit Regulations. " + "Person-level sources, such as a Lifetime ISA, count only for the " + "holder's own benunit, and only when the holder is its claimant or " + "partner (is_uc_claimant): a dependant's capital is not the claimant's." ) definition_period = YEAR unit = GBP quantity_type = STOCK + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/5", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/18", + ) def formula(benunit, period, parameters): household = benunit.household @@ -23,25 +32,25 @@ def formula(benunit, period, parameters): household_capital = sum( household(source, period) for source in p.capital.sources ) + benunit_claimants = add(benunit, period, ["is_uc_claimant"]) claimant_or_partner = benunit.members("is_uc_claimant", period) person_capital = sum( benunit.sum(benunit.members(source, period) * claimant_or_partner) for source in p.capital.person_sources ) - benunit_adults = add(benunit, period, ["is_adult"]) household_reported_capital = household("household_uc_reported_capital", period) - household_unreported_adults = household( - "household_uc_unreported_adults", period + household_unreported_claimants = household( + "household_uc_unreported_claimants", period ) - adult_divisor = max_(1, household_unreported_adults) + claimant_divisor = max_(1, household_unreported_claimants) reported_capital = benunit("uc_reported_capital", period) use_reported_capital = reported_capital >= 0 residual_household_capital = max_( 0, household_capital - household_reported_capital ) household_capital_proxy = where( - household_unreported_adults > 0, - residual_household_capital * benunit_adults / adult_divisor, + household_unreported_claimants > 0, + residual_household_capital * benunit_claimants / claimant_divisor, 0, ) assessed_capital = where( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py index 28307abe94..46dbed7c6d 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py @@ -11,8 +11,8 @@ class is_uc_work_allowance_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members has_LCWRA = benunit.any(person("uc_limited_capability_for_WRA", period)) - has_children = benunit.any( - person("is_child_or_qualifying_young_person_for_universal_credit", period) - & ~person("is_uc_claimant", period) + has_children = benunit( + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + period, ) return has_LCWRA | has_children diff --git a/policyengine_uk/variables/gov/dwp/youngest_child_age_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/youngest_child_age_for_legacy_benefits.py new file mode 100644 index 0000000000..de2c1449f3 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/youngest_child_age_for_legacy_benefits.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class youngest_child_age_for_legacy_benefits(Variable): + value_type = float + entity = BenUnit + label = "Age of youngest child for legacy means-tested benefits" + documentation = ( + "Youngest family member under 16, excluding the claimant and partner. " + "SSCBA sections 137(1) and 142(1) use the same child age definition. " + "Infinity if the benefit unit contains no such child." + ) + definition_period = YEAR + unit = "year" + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/137", + "https://www.legislation.gov.uk/ukpga/1992/4/section/142", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/1B", + ) + + def formula(benunit, period, parameters): + person = benunit.members + child = person("is_child_for_child_benefit", period) & ~person( + "is_claimant_or_partner", period + ) + return benunit.min(where(child, person("age", period), np.inf)) diff --git a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/adult_index_cg.py b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/adult_index_cg.py index 3cd2f2d849..c2d45584ce 100644 --- a/policyengine_uk/variables/gov/hmrc/capital_gains_tax/adult_index_cg.py +++ b/policyengine_uk/variables/gov/hmrc/capital_gains_tax/adult_index_cg.py @@ -5,7 +5,14 @@ class adult_index_cg(Variable): value_type = int entity = Person - label = "index of adult in household, ranked by capital gains" + label = "rank by capital gains among household members aged 18 or over" + documentation = ( + "Ranks household members aged 18 or over by capital gains, largest " + "first, starting at 1; 0 for everyone under 18. It picks whose " + "marginal capital gains tax rate is simulated (two branch simulations " + "per household). A modelling choice, not law: capital gains tax has " + "no age condition." + ) definition_period = YEAR def formula(person, period, parameters): @@ -13,7 +20,7 @@ def formula(person, period, parameters): person.get_rank( person.household, -person("capital_gains_before_response", period), - condition=person("is_adult", period), + condition=~person("age_under_18", period), ) + 1 ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py index a848ad9931..d54f76158a 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py @@ -27,10 +27,13 @@ class marriage_allowance(Variable): def formula(person, period, parameters): marital = person("marital_status", period) married = marital == marital.possible_values.MARRIED - is_adult = person("is_adult", period) + # The election is between spouses or civil partners (ITA 2007 + # s.55C(1)(a)): the claimant and partner of a married benefit unit, + # never their children. + spouse_or_civil_partner = person("is_claimant_or_partner", period) eligible = ( married - & is_adult + & spouse_or_civil_partner & person("meets_marriage_allowance_income_conditions", period) ) transferable_amount = person("partners_unused_personal_allowance", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py index 8d26ee5cb1..9f18828b9f 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py @@ -4,6 +4,7 @@ class partners_unused_personal_allowance(Variable): label = "Partner's unused personal allowance" + reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B" documentation = ( "The personal tax allowance not used by this person's partner, if they exist" ) @@ -13,9 +14,10 @@ class partners_unused_personal_allowance(Variable): unit = GBP def formula(person, period, parameters): - is_adult = person("is_adult", period) + # Only a spouse or civil partner can transfer allowance (ITA 2007 + # s.55B(5A), s.55C(1)(a)): the other claimant or partner of the + # benefit unit, never a child. People outside the couple get zero, so + # they cannot produce a negative transferable amount. + couple_member = person("is_claimant_or_partner", period) pa = person("unused_personal_allowance", period) - # Subtract this person's own unused PA only if they are an adult, so - # non-adults (whose PA isn't part of the adult-summed pool) cannot - # produce a negative transferable amount. - return person.benunit.sum(is_adult * pa) - is_adult * pa + return person.benunit.sum(couple_member * pa) - couple_member * pa diff --git a/policyengine_uk/variables/gov/hmrc/tax_free_childcare/conditions/tax_free_childcare_work_condition.py b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/conditions/tax_free_childcare_work_condition.py index d9e0dd1fb7..18352d4172 100644 --- a/policyengine_uk/variables/gov/hmrc/tax_free_childcare/conditions/tax_free_childcare_work_condition.py +++ b/policyengine_uk/variables/gov/hmrc/tax_free_childcare/conditions/tax_free_childcare_work_condition.py @@ -5,11 +5,27 @@ class tax_free_childcare_work_condition(Variable): value_type = bool entity = Person label = "work conditions for tax-free childcare" + documentation = ( + "The person applying and, if they have one, their partner must be " + "in qualifying paid work, unless the partner has a qualifying " + "disability or incapacity. Both must be at least 16. Children in the " + "family play no part." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2014/28/section/3", + "https://www.legislation.gov.uk/ukpga/2014/28/section/6", + "https://www.legislation.gov.uk/uksi/2015/448/regulation/3", + "https://www.legislation.gov.uk/uksi/2015/448/regulation/9", + ) def formula(person, period, parameters): benunit = person.benunit - is_adult = person("is_adult", period) + # The person applying and their partner (Childcare Payments Act 2014 + # s.3(1), s.6; SI 2015/448 reg 3): the claimant or partner, aged 16+. + applicant_or_partner = person("is_claimant_or_partner", period) & person( + "over_16", period + ) treated_as_in_work = person( "tax_free_childcare_treated_as_in_work", @@ -35,14 +51,16 @@ def formula(person, period, parameters): # Build conditions # Single adult conditions is_single = person.benunit("is_single", period) - single_working = is_single & treated_as_in_work + single_working = is_single & treated_as_in_work & applicant_or_partner # Couple conditions is_couple = person.benunit("is_couple", period) - benunit_has_condition = benunit.any(eligible_based_on_disability & is_adult) - benunit_has_worker = benunit.any(treated_as_in_work & is_adult) + benunit_has_condition = benunit.any( + eligible_based_on_disability & applicant_or_partner + ) + benunit_has_worker = benunit.any(treated_as_in_work & applicant_or_partner) couple_both_working = is_couple & benunit.all( - treated_as_in_work | ~person("is_parent", period) + treated_as_in_work | ~applicant_or_partner ) couple_one_working_one_disabled = ( is_couple & benunit_has_worker & benunit_has_condition 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 c096429589..8d64c045ab 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 @@ -7,11 +7,15 @@ class council_tax_reduction_applicable_amount(Variable): label = "applicable Council Tax Reduction amount" definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/7", + "https://www.legislation.gov.uk/ssi/2021/249/schedule/1", + ) def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) - eldest_age = benunit("eldest_adult_age", period) + eldest_age = benunit("eldest_claimant_or_partner_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold 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 ea624d139d..fc1e3ea7b4 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 @@ -26,6 +26,15 @@ class council_tax_reduction_applicable_income(Variable): ] 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. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_young_person_for_legacy_benefits", period + ) benunit_means_tested_benefits = [ "child_benefit", "income_support", @@ -50,21 +59,30 @@ def formula(benunit, period, parameters): "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income - benefits = add(benunit, period, benunit_means_tested_benefits) - income = add(benunit, period, income_components) - personal_benefit_income = add(benunit, period, personal_benefits) - credits = add(benunit, period, ["tax_credits"]) + benefits = add_for_members( + benunit, period, benunit_means_tested_benefits, members + ) + 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) increased_income = income + personal_benefit_income + credits + benefits if not bi.interactions.include_in_means_tests: - increased_income -= add(benunit, period, ["basic_income"]) + increased_income -= add_for_members( + benunit, period, ["basic_income"], members + ) - pension_contributions = add(benunit, period, ["pension_contributions"]) * 0.5 - tax = add(benunit, period, ["income_tax", "national_insurance"]) + pension_contributions = ( + add_for_members(benunit, period, ["pension_contributions"], members) * 0.5 + ) + tax = add_for_members( + benunit, period, ["income_tax", "national_insurance"], members + ) income_under_general_rules = max_( 0, increased_income - tax - pension_contributions ) - # SI 2012/2885 Sch 1 para 13, WSI 2013/3029 Sch 1 para 7 and SSI # 2012/319 reg 24: a guarantee credit recipient's whole income is # disregarded. Para 14, para 8 and reg 25: in savings-credit-only cases diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_non_dep_exemption.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_non_dep_exemption.py index 759aa5d7cc..3ede96b204 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_non_dep_exemption.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_non_dep_exemption.py @@ -6,11 +6,14 @@ class council_tax_reduction_household_has_non_dep_exemption(Variable): entity = Household label = "CTR household has a non-dependant deduction exemption" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8" def formula(household, period, parameters): person = household.members claimant_benunit = person.benunit("benunit_contains_household_head", period) - claimant_or_partner = claimant_benunit & person("is_adult", period) + claimant_or_partner = claimant_benunit & person( + "is_claimant_or_partner", period + ) is_blind = person("is_blind", period) & claimant_or_partner attendance_allowance = ( person("attendance_allowance", period) > 0 diff --git a/policyengine_uk/variables/household/demographic/adult_index.py b/policyengine_uk/variables/household/demographic/adult_index.py index c2ae19384f..ff6181f5c8 100644 --- a/policyengine_uk/variables/household/demographic/adult_index.py +++ b/policyengine_uk/variables/household/demographic/adult_index.py @@ -5,7 +5,13 @@ class adult_index(Variable): value_type = int entity = Person - label = "Index of adult in household" + label = "Rank among household members aged 18 or over" + documentation = ( + "Rank by age, eldest first and starting at 1, among household " + "members aged 18 or over; 0 for everyone under 18. A modelling " + "index (for example, whose marginal tax rate is simulated), not a " + "legal definition of an adult." + ) definition_period = YEAR def formula(person, period, parameters): @@ -13,7 +19,7 @@ def formula(person, period, parameters): person.get_rank( person.household, -person("age", period), - condition=person("is_adult", period), + condition=~person("age_under_18", period), ) + 1 ) diff --git a/policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py b/policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py index 9ba2239025..afc38fa39e 100644 --- a/policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py +++ b/policyengine_uk/variables/household/demographic/benunit/benunit_count_adults.py @@ -4,7 +4,12 @@ class benunit_count_adults(Variable): value_type = int entity = BenUnit - label = "number of adults in the benefit unit" + label = "Members aged 18 or over in the benefit unit (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count of is_claimant_or_partner, or of is_hbai_adult for HBAI statistics instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/benunit/benunit_count_children.py b/policyengine_uk/variables/household/demographic/benunit/benunit_count_children.py index dba3a70eb6..5e436658d4 100644 --- a/policyengine_uk/variables/household/demographic/benunit/benunit_count_children.py +++ b/policyengine_uk/variables/household/demographic/benunit/benunit_count_children.py @@ -4,7 +4,12 @@ class benunit_count_children(Variable): value_type = int entity = BenUnit - label = "number of children in the benefit unit" + label = "Members aged under 18 in the benefit unit (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count of the programme's own child definition, or of is_hbai_dependent_child for HBAI statistics instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/benunit/eldest_claimant_or_partner_age.py b/policyengine_uk/variables/household/demographic/benunit/eldest_claimant_or_partner_age.py new file mode 100644 index 0000000000..1c69df34cf --- /dev/null +++ b/policyengine_uk/variables/household/demographic/benunit/eldest_claimant_or_partner_age.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class eldest_claimant_or_partner_age(Variable): + value_type = float + entity = BenUnit + label = "Age of the elder of the claimant and partner" + documentation = ( + "The age of the claimant, or of the older member of a couple. " + "Children and young persons in the family do not count. Negative " + "infinity if the benefit unit has no claimant." + ) + definition_period = YEAR + unit = "year" + + def formula(benunit, period, parameters): + person = benunit.members + return benunit.max( + where( + person("is_claimant_or_partner", period), + person("age", period), + -np.inf, + ) + ) diff --git a/policyengine_uk/variables/household/demographic/benunit/youngest_claimant_or_partner_age.py b/policyengine_uk/variables/household/demographic/benunit/youngest_claimant_or_partner_age.py new file mode 100644 index 0000000000..83ae365600 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/benunit/youngest_claimant_or_partner_age.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class youngest_claimant_or_partner_age(Variable): + value_type = float + entity = BenUnit + label = "Age of the younger of the claimant and partner" + documentation = ( + "The age of the claimant, or of the younger member of a couple. " + "Children and young persons in the family do not count. Infinity if " + "the benefit unit has no claimant." + ) + definition_period = YEAR + unit = "year" + + def formula(benunit, period, parameters): + person = benunit.members + return benunit.min( + where( + person("is_claimant_or_partner", period), + person("age", period), + np.inf, + ) + ) diff --git a/policyengine_uk/variables/household/demographic/child_index.py b/policyengine_uk/variables/household/demographic/child_index.py index 8548ba6cd0..7850a1e075 100644 --- a/policyengine_uk/variables/household/demographic/child_index.py +++ b/policyengine_uk/variables/household/demographic/child_index.py @@ -5,7 +5,12 @@ class child_index(Variable): value_type = int entity = Person - label = "Child reference number" + label = "Rank by age among benefit-unit members under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the programme's own child index (for example uc_child_index) instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/disability_premium.py b/policyengine_uk/variables/household/demographic/disability_premium.py index e26f49195a..f151798fac 100644 --- a/policyengine_uk/variables/household/demographic/disability_premium.py +++ b/policyengine_uk/variables/household/demographic/disability_premium.py @@ -5,9 +5,17 @@ class disability_premium(Variable): value_type = float entity = BenUnit label = "Disability premium" + documentation = ( + "Legacy benefit disability premium for the claimant or partner, using " + "the model's existing disability indicator. The separate disabled " + "child premium is not modelled here." + ) definition_period = YEAR reference = ( - "The Social Security Amendment (Enhanced Disability Premium) Regulations 2000" + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/11", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/12", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/12", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/13", ) unit = GBP @@ -17,6 +25,9 @@ def formula(benunit, period, parameters): couple = benunit("is_couple", period.this_year) single_premium = single * dis.disability_single couple_premium = couple * dis.disability_couple - has_disabled_adults = benunit("num_disabled_adults", period.this_year) > 0 + has_disabled_claimant_or_partner = ( + benunit("num_disabled_claimants_or_partners_for_legacy_benefits", period) + > 0 + ) weekly_amount = single_premium + couple_premium - return weekly_amount * WEEKS_IN_YEAR * has_disabled_adults + return weekly_amount * WEEKS_IN_YEAR * has_disabled_claimant_or_partner diff --git a/policyengine_uk/variables/household/demographic/eldest_adult_age.py b/policyengine_uk/variables/household/demographic/eldest_adult_age.py index de775e51e5..7eccb02424 100644 --- a/policyengine_uk/variables/household/demographic/eldest_adult_age.py +++ b/policyengine_uk/variables/household/demographic/eldest_adult_age.py @@ -4,7 +4,12 @@ class eldest_adult_age(Variable): value_type = float entity = BenUnit - label = "Eldest adult age" + label = "Age of the eldest benefit-unit member aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use eldest_claimant_or_partner_age instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/eldest_child_age.py b/policyengine_uk/variables/household/demographic/eldest_child_age.py index d471c19e6a..ae1cb74105 100644 --- a/policyengine_uk/variables/household/demographic/eldest_child_age.py +++ b/policyengine_uk/variables/household/demographic/eldest_child_age.py @@ -4,7 +4,12 @@ class eldest_child_age(Variable): value_type = float entity = BenUnit - label = "Eldest adult age" + label = "Age of the eldest benefit-unit member under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the programme's own child definition instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/enhanced_disability_premium.py b/policyengine_uk/variables/household/demographic/enhanced_disability_premium.py index c2f8d7c4fd..296ab7b49e 100644 --- a/policyengine_uk/variables/household/demographic/enhanced_disability_premium.py +++ b/policyengine_uk/variables/household/demographic/enhanced_disability_premium.py @@ -5,9 +5,16 @@ class enhanced_disability_premium(Variable): value_type = float entity = BenUnit label = "Enhanced disability premium" + documentation = ( + "Legacy benefit enhanced disability premium for the claimant or " + "partner, using the model's existing enhanced-disability indicator. " + "The separate Housing Benefit enhanced rate for children and young " + "persons is not modelled here." + ) definition_period = YEAR reference = ( - "The Social Security Amendment (Enhanced Disability Premium) Regulations 2000" + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/13A", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/15", ) unit = GBP @@ -17,8 +24,12 @@ def formula(benunit, period, parameters): couple = benunit("is_couple", period.this_year) single_premium = single * dis.enhanced_single couple_premium = couple * dis.enhanced_couple - has_enhanced_disabled_adults = ( - benunit("num_enhanced_disabled_adults", period.this_year) > 0 + has_enhanced_disabled_claimant_or_partner = ( + benunit( + "num_enhanced_disabled_claimants_or_partners_for_legacy_benefits", + period, + ) + > 0 ) weekly_amount = single_premium + couple_premium - return weekly_amount * WEEKS_IN_YEAR * has_enhanced_disabled_adults + return weekly_amount * WEEKS_IN_YEAR * has_enhanced_disabled_claimant_or_partner diff --git a/policyengine_uk/variables/household/demographic/family_type.py b/policyengine_uk/variables/household/demographic/family_type.py index 26fb8e9134..eed1b138fd 100644 --- a/policyengine_uk/variables/household/demographic/family_type.py +++ b/policyengine_uk/variables/household/demographic/family_type.py @@ -13,7 +13,12 @@ class family_type(Variable): entity = BenUnit default_value = FamilyType.SINGLE possible_values = FamilyType - label = "Family composition" + label = "Family composition by age 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use relation_type with the programme's own responsibility for a child or young person (for example is_lone_parent for the legacy benefits), or hbai_person_type for HBAI statistics instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/hbai/hbai_person_type.py b/policyengine_uk/variables/household/demographic/hbai/hbai_person_type.py new file mode 100644 index 0000000000..082139025f --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/hbai_person_type.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class HBAIPersonType(Enum): + CHILD = "Dependent child" + WORKING_AGE_ADULT = "Working-age adult" + PENSIONER = "Pensioner" + + +class hbai_person_type(Variable): + value_type = Enum + possible_values = HBAIPersonType + default_value = HBAIPersonType.WORKING_AGE_ADULT + entity = Person + label = "Population group (HBAI definition)" + documentation = ( + "The Households Below Average Income population groups: dependent " + "children, working-age adults (below State Pension age) and " + "pensioners (at or above State Pension age). Every person is in " + "exactly one group." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#child" + + def formula(person, period, parameters): + return select( + [ + person("is_hbai_dependent_child", period), + person("is_hbai_working_age_adult", period), + ], + [HBAIPersonType.CHILD, HBAIPersonType.WORKING_AGE_ADULT], + default=HBAIPersonType.PENSIONER, + ) diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_adult.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_adult.py new file mode 100644 index 0000000000..9008d4aabe --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_adult.py @@ -0,0 +1,16 @@ +from policyengine_uk.model_api import * + + +class is_hbai_adult(Variable): + value_type = bool + entity = Person + label = "Adult (HBAI definition)" + documentation = ( + "Households Below Average Income counts everyone who is not a " + "dependent child as an adult." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#adult" + + def formula(person, period, parameters): + return ~person("is_hbai_dependent_child", period) diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_aged_14_or_over.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_aged_14_or_over.py new file mode 100644 index 0000000000..4226577276 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_aged_14_or_over.py @@ -0,0 +1,19 @@ +from policyengine_uk.model_api import * + + +class is_hbai_child_aged_14_or_over(Variable): + value_type = bool + entity = Person + label = "Dependent child aged 14 or over (HBAI equivalence scale)" + documentation = ( + "A dependent child at or above the older-child age of the HBAI " + "modified OECD equivalence scale." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#equivalisation-1" + + def formula(person, period, parameters): + threshold = parameters(period).household.demographic.equiv.child_age_threshold + return person("is_hbai_dependent_child", period) & ( + person("age", period) >= threshold + ) diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_under_14.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_under_14.py new file mode 100644 index 0000000000..7595f85ead --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_child_under_14.py @@ -0,0 +1,19 @@ +from policyengine_uk.model_api import * + + +class is_hbai_child_under_14(Variable): + value_type = bool + entity = Person + label = "Dependent child under 14 (HBAI equivalence scale)" + documentation = ( + "A dependent child below the older-child age of the HBAI modified " + "OECD equivalence scale." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#equivalisation-1" + + def formula(person, period, parameters): + threshold = parameters(period).household.demographic.equiv.child_age_threshold + return person("is_hbai_dependent_child", period) & ( + person("age", period) < threshold + ) diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_dependent_child.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_dependent_child.py new file mode 100644 index 0000000000..8405d73814 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_dependent_child.py @@ -0,0 +1,59 @@ +from policyengine_uk.model_api import * + + +class is_hbai_dependent_child(Variable): + """Dependent child as defined for Households Below Average Income (HBAI). + + HBAI (and the Family Resources Survey, whose benefit units it uses) + counts a person as a dependent child if they are under 16, or aged 16 + to 19 and not married, in a civil partnership or living with a partner, + living with parents or a responsible adult, and in full-time + non-advanced education or unwaged government training. + + Datasets built from the FRS should supply this directly: it is exactly + the FRS child-table record. The formula is a calculator fallback that + infers the definition from benefit-unit structure: + + - everyone under 16 is a dependent child; + - a 16- or 17-year-old who is neither the benefit-unit head nor an + identified parent is treated as a dependent child. This keeps the + previous age-18 treatment: the FRS puts non-dependent 16- and + 17-year-olds in their own benefit unit, so a non-head in that age + group is normally an FRS dependent child, whatever the education + inputs say; + - an 18- or 19-year-old who is neither the head nor an identified parent + is a dependent child only if they are in non-advanced education or + approved training and the benefit unit contains an identified parent + (`is_parent`, aged 16 or over). Without an identified parent the model cannot tell a + dependant from a partner, so it keeps treating them as an adult. + + This is a statistical definition. Benefit and tax rules use each + programme's own legal definition of a child. + """ + + value_type = bool + entity = Person + label = "Dependent child (HBAI definition)" + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#child" + + def formula(person, period, parameters): + p = parameters(period).household.demographic.hbai.dependent_child + age = person("age", period) + is_parent = person("is_parent", period) + lives_as_dependant = ~person("is_benunit_head", period) & ~is_parent + in_education_or_training = person( + "is_in_non_advanced_education", period + ) | person("is_in_approved_training", period) + under_child_age = age < p.age_limit + # An identified parent is a benefit-unit member aged 16 or over + # flagged as a parent; a parent flag on a child is ignored. + lives_with_identified_parent = person.benunit.any(is_parent & ~under_child_age) + under_18_dependant = lives_as_dependant & person("age_under_18", period) + young_person = ( + lives_as_dependant + & (age < p.young_person_age_limit) + & in_education_or_training + & lives_with_identified_parent + ) + return under_child_age | under_18_dependant | young_person diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_pensioner.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_pensioner.py new file mode 100644 index 0000000000..802eb4c2e1 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_pensioner.py @@ -0,0 +1,16 @@ +from policyengine_uk.model_api import * + + +class is_hbai_pensioner(Variable): + value_type = bool + entity = Person + label = "Pensioner (HBAI definition)" + documentation = ( + "Households Below Average Income defines pensioners as all adults at " + "or above State Pension age, classified individually." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#pensioner" + + def formula(person, period, parameters): + return person("is_hbai_adult", period) & person("is_SP_age", period) diff --git a/policyengine_uk/variables/household/demographic/hbai/is_hbai_working_age_adult.py b/policyengine_uk/variables/household/demographic/hbai/is_hbai_working_age_adult.py new file mode 100644 index 0000000000..1d06fcf0cb --- /dev/null +++ b/policyengine_uk/variables/household/demographic/hbai/is_hbai_working_age_adult.py @@ -0,0 +1,16 @@ +from policyengine_uk.model_api import * + + +class is_hbai_working_age_adult(Variable): + value_type = bool + entity = Person + label = "Working-age adult (HBAI definition)" + documentation = ( + "Households Below Average Income defines working-age adults as all " + "adults below State Pension age." + ) + definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#working-age-adults" + + def formula(person, period, parameters): + return person("is_hbai_adult", period) & ~person("is_SP_age", period) diff --git a/policyengine_uk/variables/household/demographic/household_equivalisation_ahc.py b/policyengine_uk/variables/household/demographic/household_equivalisation_ahc.py index 6be6286ef8..075f1de0a4 100644 --- a/policyengine_uk/variables/household/demographic/household_equivalisation_ahc.py +++ b/policyengine_uk/variables/household/demographic/household_equivalisation_ahc.py @@ -5,18 +5,25 @@ class household_equivalisation_ahc(Variable): value_type = float entity = Household label = "Equivalisation factor to account for household composition, after housing costs" + documentation = ( + "The HBAI modified OECD equivalence scale (AHC): a weight for the first " + "adult, a weight for each other adult, and weights for dependent " + "children under 14 and aged 14 or over. Adults and dependent children " + "follow the HBAI definitions." + ) definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#equivalisation-1" def formula(household, period, parameters): scale = parameters(period).household.demographic.equiv.ahc count_other_adults = max_( - household.sum(household.members("is_adult", period)) - 1, 0 + household.sum(household.members("is_hbai_adult", period)) - 1, 0 ) count_young_children = household.sum( - household.members("is_young_child", period) + household.members("is_hbai_child_under_14", period) ) count_older_children = household.sum( - household.members("is_older_child", period) + household.members("is_hbai_child_aged_14_or_over", period) ) return ( scale.first_adult diff --git a/policyengine_uk/variables/household/demographic/household_equivalisation_bhc.py b/policyengine_uk/variables/household/demographic/household_equivalisation_bhc.py index a133022af6..0c77033de9 100644 --- a/policyengine_uk/variables/household/demographic/household_equivalisation_bhc.py +++ b/policyengine_uk/variables/household/demographic/household_equivalisation_bhc.py @@ -5,18 +5,25 @@ class household_equivalisation_bhc(Variable): value_type = float entity = Household label = "Equivalisation factor to account for household composition, before housing costs" + documentation = ( + "The HBAI modified OECD equivalence scale (BHC): a weight for the first " + "adult, a weight for each other adult, and weights for dependent " + "children under 14 and aged 14 or over. Adults and dependent children " + "follow the HBAI definitions." + ) definition_period = YEAR + reference = "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025#equivalisation-1" def formula(household, period, parameters): scale = parameters(period).household.demographic.equiv.bhc count_other_adults = max_( - household.sum(household.members("is_adult", period)) - 1, 0 + household.sum(household.members("is_hbai_adult", period)) - 1, 0 ) count_young_children = household.sum( - household.members("is_young_child", period) + household.members("is_hbai_child_under_14", period) ) count_older_children = household.sum( - household.members("is_older_child", period) + household.members("is_hbai_child_aged_14_or_over", period) ) return ( scale.first_adult diff --git a/policyengine_uk/variables/household/demographic/is_WA_adult.py b/policyengine_uk/variables/household/demographic/is_WA_adult.py index 8db33a6dbe..04004d1ad0 100644 --- a/policyengine_uk/variables/household/demographic/is_WA_adult.py +++ b/policyengine_uk/variables/household/demographic/is_WA_adult.py @@ -5,7 +5,12 @@ class is_WA_adult(Variable): value_type = bool entity = Person - label = "Whether is a working-age adult" + label = "Aged 18 to below State Pension age (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use is_hbai_working_age_adult for HBAI statistics, or the programme's own age conditions instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/is_adult.py b/policyengine_uk/variables/household/demographic/is_adult.py index 29bb92d537..cd0e67cb95 100644 --- a/policyengine_uk/variables/household/demographic/is_adult.py +++ b/policyengine_uk/variables/household/demographic/is_adult.py @@ -5,7 +5,12 @@ class is_adult(Variable): value_type = bool entity = Person - label = "Whether this person is an adult" + label = "Aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the negation of age_under_18 for the age band, is_hbai_adult for HBAI statistics, or is_claimant_or_partner for the claimant and partner of a benefit unit instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/is_benunit_eldest_child.py b/policyengine_uk/variables/household/demographic/is_benunit_eldest_child.py index 58e883aabe..b4589cf46e 100644 --- a/policyengine_uk/variables/household/demographic/is_benunit_eldest_child.py +++ b/policyengine_uk/variables/household/demographic/is_benunit_eldest_child.py @@ -5,7 +5,12 @@ class is_benunit_eldest_child(Variable): value_type = bool entity = Person - label = "Eldest child in the benefit unit" + label = "Eldest benefit-unit member under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the programme's own child index instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/is_child.py b/policyengine_uk/variables/household/demographic/is_child.py index 675c320d2e..8f0dad7018 100644 --- a/policyengine_uk/variables/household/demographic/is_child.py +++ b/policyengine_uk/variables/household/demographic/is_child.py @@ -5,7 +5,12 @@ class is_child(Variable): value_type = bool entity = Person - label = "Is a child" + label = "Aged under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use age_under_18 for the age band, is_hbai_dependent_child for HBAI statistics, or the programme's own child definition (for example is_child_or_qualifying_young_person_for_universal_credit) instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py new file mode 100644 index 0000000000..f41fbc3db5 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py @@ -0,0 +1,73 @@ +from policyengine_uk.model_api import * + + +class is_claimant_or_partner(Variable): + """The single adult or couple a benefit unit is formed around. + + In benefit law this is the claimant and their partner, as opposed to the + children and young persons they are responsible for (SSCBA 1992 s.137(1) + "family"; WRA 2012 ss.39-40; SPCA 2002 s.17; TCA 2002 s.3). The same two + people are the spouses or civil partners for income tax purposes when the + benefit unit is a married couple. A benefit unit has one claimant, or a + couple: never more than two. + + Datasets or users can supply this directly. Otherwise it is inferred from + benefit-unit structure, which follows the Family Resources Survey: a single + adult or a couple plus dependent children. + + - Only HBAI adults (`is_hbai_adult`) can be a claimant or partner. + - The claimant is the benefit-unit head (the eldest adult head if more + than one is given), or the eldest adult if the head is not an adult. + - The partner is one other member: the eldest other member flagged as a + parent (`is_parent`) if there is one, otherwise the eldest other adult + who is not presumed to be the claimant's child. A member under 20 and at + least 16 years younger than the claimant is presumed to be their child + (a PolicyEngine presumption for households entered without + relationships). + - If the claimant is not flagged as a parent but two or more other members + are, the two eldest of those are the claimant and partner instead (for + example parents living in a grandparent's benefit unit). + - Any further adults are neither claimant nor partner. + """ + + value_type = bool + entity = Person + label = "Claimant or partner" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/137", + "https://www.legislation.gov.uk/ukpga/2012/5/section/39", + "https://www.legislation.gov.uk/ukpga/2002/16/section/17", + "https://www.legislation.gov.uk/ukpga/2002/21/section/3", + ) + + def formula(person, period, parameters): + p = parameters(period).household.demographic.benefit_unit.presumed_child + age = person("age", period) + adult = person("is_hbai_adult", period) + is_head = person("is_benunit_head", period) + head_is_adult = person.benunit.any(is_head & adult) + eldest_adult = adult & ( + person.get_rank(person.benunit, -age, condition=adult) == 0 + ) + # With more than one head (malformed input), the eldest adult head. + adult_head = is_head & adult + eldest_adult_head = adult_head & ( + person.get_rank(person.benunit, -age, condition=adult_head) == 0 + ) + claimant = where(head_is_adult, eldest_adult_head, eldest_adult) + claimant_age = person.benunit.max(where(claimant, age, -np.inf)) + identified_parent = adult & person("is_parent", period) + other_parent = identified_parent & ~claimant + # Two flagged parents other than a non-parent claimant are the couple. + parents_are_couple = (person.benunit.sum(other_parent) >= 2) & ~( + person.benunit.any(claimant & identified_parent) + ) + parent_couple = other_parent & ( + person.get_rank(person.benunit, -age, condition=other_parent) < 2 + ) + presumed_child = (age < p.age_limit) & (claimant_age - age >= p.minimum_age_gap) + other_adult = adult & ~claimant & ~presumed_child + pool = where(person.benunit.any(other_parent), other_parent, other_adult) + partner = pool & (person.get_rank(person.benunit, -age, condition=pool) == 0) + return where(parents_are_couple, parent_couple, claimant | partner) diff --git a/policyengine_uk/variables/household/demographic/is_eldest_child.py b/policyengine_uk/variables/household/demographic/is_eldest_child.py index 91a13a0e0b..0ce0204001 100644 --- a/policyengine_uk/variables/household/demographic/is_eldest_child.py +++ b/policyengine_uk/variables/household/demographic/is_eldest_child.py @@ -3,8 +3,12 @@ class is_eldest_child(Variable): - label = "Is the eldest child" - documentation = "Whether this person is the eldest child in the benefit unit" + label = "Eldest benefit-unit member under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the programme's own child index instead." + ) entity = Person definition_period = YEAR value_type = bool diff --git a/policyengine_uk/variables/household/demographic/is_married.py b/policyengine_uk/variables/household/demographic/is_married.py index 6f234b4ae1..7559be4bc7 100644 --- a/policyengine_uk/variables/household/demographic/is_married.py +++ b/policyengine_uk/variables/household/demographic/is_married.py @@ -5,8 +5,13 @@ class is_married(Variable): value_type = bool entity = BenUnit label = "Married" - documentation = "Whether the benefit unit adults are married to each other or in a civil partnership" + documentation = ( + "Whether the benefit unit's couple are married to each other or in a " + "civil partnership. Datasets supply this from the survey. Without " + "that input, the calculator presumes every couple is married, since " + "marriage cannot be told apart from cohabitation." + ) definition_period = YEAR def formula(benunit, period, parameters): - return add(benunit, period, ["is_adult"]) == 2 + return benunit("is_couple", period) diff --git a/policyengine_uk/variables/household/demographic/is_older_child.py b/policyengine_uk/variables/household/demographic/is_older_child.py index a7555133fd..e0e142f233 100644 --- a/policyengine_uk/variables/household/demographic/is_older_child.py +++ b/policyengine_uk/variables/household/demographic/is_older_child.py @@ -5,7 +5,12 @@ class is_older_child(Variable): value_type = bool entity = Person - label = "Whether the person is over 14 but under 18" + label = "Aged 14 to 17 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use is_hbai_child_aged_14_or_over instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/is_young_child.py b/policyengine_uk/variables/household/demographic/is_young_child.py index a4708d441f..c7d3d5962e 100644 --- a/policyengine_uk/variables/household/demographic/is_young_child.py +++ b/policyengine_uk/variables/household/demographic/is_young_child.py @@ -5,7 +5,12 @@ class is_young_child(Variable): value_type = bool entity = Person - label = "Whether the person is under 14" + label = "Aged under 14 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use is_hbai_child_under_14 instead." + ) definition_period = YEAR def formula(person, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/marital_status.py b/policyengine_uk/variables/household/demographic/marital_status.py index e70c06fafa..d50ad9bc3b 100644 --- a/policyengine_uk/variables/household/demographic/marital_status.py +++ b/policyengine_uk/variables/household/demographic/marital_status.py @@ -16,11 +16,17 @@ class marital_status(Variable): default_value = MaritalStatus.SINGLE entity = Person label = "Marital status" + documentation = ( + "Datasets supply this from the survey. Without that input, the " + "claimant and partner of a married benefit unit are married and " + "everyone else, including their children, is single." + ) definition_period = YEAR def formula(person, period, parameters): return where( - person.benunit("is_married", period), + person.benunit("is_married", period) + & person("is_claimant_or_partner", period), MaritalStatus.MARRIED, MaritalStatus.SINGLE, ) diff --git a/policyengine_uk/variables/household/demographic/num_adults.py b/policyengine_uk/variables/household/demographic/num_adults.py index 5697db5cb0..945a193dbb 100644 --- a/policyengine_uk/variables/household/demographic/num_adults.py +++ b/policyengine_uk/variables/household/demographic/num_adults.py @@ -4,7 +4,12 @@ class num_adults(Variable): value_type = int entity = BenUnit - label = "The number of adults in the family" + label = "Members aged 18 or over in the benefit unit (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count of is_claimant_or_partner, or of is_hbai_adult for HBAI statistics instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_children.py b/policyengine_uk/variables/household/demographic/num_children.py index b9e33ef214..e8175fe7db 100644 --- a/policyengine_uk/variables/household/demographic/num_children.py +++ b/policyengine_uk/variables/household/demographic/num_children.py @@ -4,7 +4,12 @@ class num_children(Variable): value_type = int entity = BenUnit - label = "The number of children in the family" + label = "Members aged under 18 in the benefit unit (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count of the programme's own child definition, or of is_hbai_dependent_child for HBAI statistics instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_disabled_adults.py b/policyengine_uk/variables/household/demographic/num_disabled_adults.py index 0bf0731b02..c83e14eb90 100644 --- a/policyengine_uk/variables/household/demographic/num_disabled_adults.py +++ b/policyengine_uk/variables/household/demographic/num_disabled_adults.py @@ -4,7 +4,12 @@ class num_disabled_adults(Variable): value_type = int entity = BenUnit - label = "Number of disabled adults" + label = "Disabled benefit-unit members aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over is_claimant_or_partner instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_disabled_children.py b/policyengine_uk/variables/household/demographic/num_disabled_children.py index 5d6fcd8c05..376215ea07 100644 --- a/policyengine_uk/variables/household/demographic/num_disabled_children.py +++ b/policyengine_uk/variables/household/demographic/num_disabled_children.py @@ -4,7 +4,12 @@ class num_disabled_children(Variable): value_type = int entity = BenUnit - label = "Number of disabled children" + label = "Disabled benefit-unit members under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over the programme's own child definition instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_enhanced_disabled_adults.py b/policyengine_uk/variables/household/demographic/num_enhanced_disabled_adults.py index 8dc90fe17a..71a26bee50 100644 --- a/policyengine_uk/variables/household/demographic/num_enhanced_disabled_adults.py +++ b/policyengine_uk/variables/household/demographic/num_enhanced_disabled_adults.py @@ -4,7 +4,12 @@ class num_enhanced_disabled_adults(Variable): value_type = int entity = BenUnit - label = "Number of enhanced disabled adults" + label = "Enhanced-disabled benefit-unit members aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over is_claimant_or_partner instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_enhanced_disabled_children.py b/policyengine_uk/variables/household/demographic/num_enhanced_disabled_children.py index 3ef0002725..c5bc8e454e 100644 --- a/policyengine_uk/variables/household/demographic/num_enhanced_disabled_children.py +++ b/policyengine_uk/variables/household/demographic/num_enhanced_disabled_children.py @@ -4,7 +4,12 @@ class num_enhanced_disabled_children(Variable): value_type = int entity = BenUnit - label = "Number of enhanced disabled children" + label = "Enhanced-disabled benefit-unit members under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over the programme's own child definition instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_severely_disabled_adults.py b/policyengine_uk/variables/household/demographic/num_severely_disabled_adults.py index fef66b4f0a..b574c01285 100644 --- a/policyengine_uk/variables/household/demographic/num_severely_disabled_adults.py +++ b/policyengine_uk/variables/household/demographic/num_severely_disabled_adults.py @@ -4,7 +4,12 @@ class num_severely_disabled_adults(Variable): value_type = int entity = BenUnit - label = "Number of severely disabled adults" + label = "Severely disabled benefit-unit members aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over is_claimant_or_partner instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/num_severely_disabled_children.py b/policyengine_uk/variables/household/demographic/num_severely_disabled_children.py index ce4abd9d52..1be5bd7934 100644 --- a/policyengine_uk/variables/household/demographic/num_severely_disabled_children.py +++ b/policyengine_uk/variables/household/demographic/num_severely_disabled_children.py @@ -4,7 +4,12 @@ class num_severely_disabled_children(Variable): value_type = int entity = BenUnit - label = "Number of severely disabled children" + label = "Severely disabled benefit-unit members under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use a count over the programme's own child definition instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/relation_type.py b/policyengine_uk/variables/household/demographic/relation_type.py index ae32d6c625..9bdb157b32 100644 --- a/policyengine_uk/variables/household/demographic/relation_type.py +++ b/policyengine_uk/variables/household/demographic/relation_type.py @@ -12,11 +12,21 @@ class relation_type(Variable): default_value = RelationType.SINGLE possible_values = RelationType label = "Whether single or a couple" + documentation = ( + "Whether the claimant has a partner. A couple is two people who are " + "married or civil partners in the same household, or who live " + "together as if they were; the same definition applies across " + "benefit and tax credit law. Dependent children and young persons " + "are never the partner." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/137", + "https://www.legislation.gov.uk/ukpga/2012/5/section/39", + "https://www.legislation.gov.uk/ukpga/2002/16/section/17", + "https://www.legislation.gov.uk/ukpga/2002/21/section/3", + ) def formula(benunit, period, parameters): - return where( - benunit.sum(benunit.members("is_adult", period)) == 1, - RelationType.SINGLE, - RelationType.COUPLE, - ) + claimants = add(benunit, period, ["is_claimant_or_partner"]) + return where(claimants >= 2, RelationType.COUPLE, RelationType.SINGLE) diff --git a/policyengine_uk/variables/household/demographic/severe_disability_premium.py b/policyengine_uk/variables/household/demographic/severe_disability_premium.py index c8501f4e11..d2de6c640c 100644 --- a/policyengine_uk/variables/household/demographic/severe_disability_premium.py +++ b/policyengine_uk/variables/household/demographic/severe_disability_premium.py @@ -5,9 +5,17 @@ class severe_disability_premium(Variable): value_type = float entity = BenUnit label = "Severe disability premium" + documentation = ( + "Legacy benefit severe disability premium using the model's existing " + "severe-disability indicator for the claimant or partner. The current " + "approximation pays the couple rate when either partner qualifies; " + "the statutory requirement for both partners to qualify and the " + "non-dependant and carer exclusions remain unmodelled." + ) definition_period = YEAR reference = ( - "The Social Security Amendment (Enhanced Disability Premium) Regulations 2000" + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2/paragraph/13", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14", ) unit = GBP @@ -17,8 +25,12 @@ def formula(benunit, period, parameters): couple = benunit("is_couple", period.this_year) single_premium = single * dis.severe_single couple_premium = couple * dis.severe_couple - has_severely_disabled_adults = ( - benunit("num_severely_disabled_adults", period.this_year) > 0 + has_severely_disabled_claimant_or_partner = ( + benunit( + "num_severely_disabled_claimants_or_partners_for_legacy_benefits", + period, + ) + > 0 ) weekly_amount = single_premium + couple_premium - return weekly_amount * WEEKS_IN_YEAR * has_severely_disabled_adults + return weekly_amount * WEEKS_IN_YEAR * has_severely_disabled_claimant_or_partner diff --git a/policyengine_uk/variables/household/demographic/youngest_adult_age.py b/policyengine_uk/variables/household/demographic/youngest_adult_age.py index 0f51550fd0..caa332365e 100644 --- a/policyengine_uk/variables/household/demographic/youngest_adult_age.py +++ b/policyengine_uk/variables/household/demographic/youngest_adult_age.py @@ -4,7 +4,12 @@ class youngest_adult_age(Variable): value_type = float entity = BenUnit - label = "Eldest adult age" + label = "Age of the youngest benefit-unit member aged 18 or over (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use youngest_claimant_or_partner_age instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/demographic/youngest_child_age.py b/policyengine_uk/variables/household/demographic/youngest_child_age.py index 32406e7b5f..83ae608e62 100644 --- a/policyengine_uk/variables/household/demographic/youngest_child_age.py +++ b/policyengine_uk/variables/household/demographic/youngest_child_age.py @@ -4,7 +4,12 @@ class youngest_child_age(Variable): value_type = float entity = BenUnit - label = "Eldest adult age" + label = "Age of the youngest benefit-unit member under 18 (deprecated)" + documentation = ( + "Deprecated: an age cut-off with no legal basis, kept only for " + "downstream compatibility with its original formula. Nothing in " + "policyengine-uk uses it. Use the programme's own child definition instead." + ) definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/income/is_couple.py b/policyengine_uk/variables/household/income/is_couple.py index 236a1da10c..e8b8bc7ea1 100644 --- a/policyengine_uk/variables/household/income/is_couple.py +++ b/policyengine_uk/variables/household/income/is_couple.py @@ -4,7 +4,8 @@ class is_couple(Variable): value_type = bool entity = BenUnit - label = "Whether this benefit unit contains a joint couple claimant for benefits" + label = "Claimant has a partner" + documentation = "Whether the claimant is a member of a couple." definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/income/is_lone_parent.py b/policyengine_uk/variables/household/income/is_lone_parent.py index 07ffdcb0b3..613e885de6 100644 --- a/policyengine_uk/variables/household/income/is_lone_parent.py +++ b/policyengine_uk/variables/household/income/is_lone_parent.py @@ -4,10 +4,20 @@ class is_lone_parent(Variable): value_type = bool entity = BenUnit - label = "Whether the family is a lone parent family" + label = "Lone parent" + documentation = ( + "A claimant with no partner who is responsible for a child or young " + "person in the same household, as defined for Income Support, " + "Housing Benefit and council tax reduction." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + ) def formula(benunit, period, parameters): - family_type = benunit("family_type", period) - families = family_type.possible_values - return family_type == families.LONE_PARENT + return benunit("is_single", period) & benunit( + "is_responsible_for_child_or_young_person_for_legacy_benefits", period + ) diff --git a/policyengine_uk/variables/household/income/is_single.py b/policyengine_uk/variables/household/income/is_single.py index b226945fe7..db2a623ef6 100644 --- a/policyengine_uk/variables/household/income/is_single.py +++ b/policyengine_uk/variables/household/income/is_single.py @@ -4,7 +4,8 @@ class is_single(Variable): value_type = bool entity = BenUnit - label = "Whether this benefit unit contains a single claimant for benefits" + label = "Claimant has no partner" + documentation = "Whether the claimant is not a member of a couple." definition_period = YEAR def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/household/income/is_single_person.py b/policyengine_uk/variables/household/income/is_single_person.py index b8da33b4e9..c17437f4af 100644 --- a/policyengine_uk/variables/household/income/is_single_person.py +++ b/policyengine_uk/variables/household/income/is_single_person.py @@ -4,10 +4,15 @@ class is_single_person(Variable): value_type = bool entity = BenUnit - label = "Whether the family is a single person" + label = "Single claimant" + documentation = ( + "A claimant who has no partner and is not a lone parent, as defined " + "for Housing Benefit and council tax reduction." + ) definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/2" def formula(benunit, period, parameters): - family_type = benunit("family_type", period) - families = family_type.possible_values - return family_type == families.SINGLE + return benunit("is_single", period) & ~benunit( + "is_responsible_for_child_or_young_person_for_legacy_benefits", period + )