From 538c4d614aa41ed6f14c290c1713fe6aaf9b775a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 11:01:38 +0000 Subject: [PATCH 1/6] Pay carer benefits on hours only for care of a severely disabled person SSCBA 1992 s.70(1) and (2) give Carer's Allowance only for caring for a severely disabled person, SSI 2023/302 reg 5(1) pays Carer Support Payment only for care of a person to whom a qualifying disability benefit is normally payable, and UC Regs 2013 reg 30(1)(a) applies the Carer's Allowance conditions to the carer element. The model tested caring hours alone. Add the person input cares_for_severely_disabled_person, which defaults to true because survey caring hours don't say whom the carer cares for. The hours route in carers_allowance_weekly_by_part_of_year, carer_support_payment_pre_couple_limit, is_carer_for_benefits and the explained_by_hours test of partners_care_for_same_severely_disabled_person now needs it. Reported awards still count. Fixes #2310 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2310.fixed.md | 1 + .../carers_allowance_cared_for_person.yaml | 400 ++++++++++++++++++ ...for_severely_disabled_person_properties.py | 382 +++++++++++++++++ .../utils/carers_allowance_taper.py | 6 +- .../dwp/cares_for_severely_disabled_person.py | 59 +++ .../uc_unable_to_provide_childcare.py | 10 +- .../meets_uc_minimum_age_condition.py | 5 +- .../carer_support_payment_pre_couple_limit.py | 8 +- .../demographic/is_carer_for_benefits.py | 29 +- ..._care_for_same_severely_disabled_person.py | 31 +- 10 files changed, 904 insertions(+), 27 deletions(-) create mode 100644 changelog.d/2310.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml create mode 100644 policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py diff --git a/changelog.d/2310.fixed.md b/changelog.d/2310.fixed.md new file mode 100644 index 0000000000..4a3bda8989 --- /dev/null +++ b/changelog.d/2310.fixed.md @@ -0,0 +1 @@ +Pay Carer's Allowance and Carer Support Payment, and count a carer for the Universal Credit carer element, on caring hours only where the new input `cares_for_severely_disabled_person` says the person cared for gets a qualifying disability benefit; it defaults to true, so results change only where a calculation sets it false. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml new file mode 100644 index 0000000000..f676012ff8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml @@ -0,0 +1,400 @@ +# Carer benefits for caring for a severely disabled person. +# SSCBA 1992 s.70(1): "A person shall be entitled to a carer's allowance for +# any day on which he is engaged in caring for a severely disabled person". +# s.70(2) defines a "severely disabled person" by the benefit payable for them: +# attendance allowance, the disability living allowance care component at the +# highest or middle rate, the personal independence payment daily living +# component, armed forces independence payment, the Scottish disability +# payments it lists, or a payment that SI 1976/409 reg 3 prescribes. +# SSI 2023/302 reg 5(1): "Carer Support Payment may only be paid to an +# individual in respect of a period during which that individual provides +# regular and substantial care to a person to whom a qualifying disability +# benefit is normally payable". UC Regs 2013 reg 30(1)(a) gives the carer +# element to a person who "satisfy the conditions for entitlement to a +# carer's allowance", or would but for the earnings limit or residence in +# Scotland. +# +# cares_for_severely_disabled_person records whether the person cared for +# gets such a benefit. It defaults to true. False removes the route through +# caring hours; a reported award (carers_allowance_reported) still counts. +# +# The model reads each parameter at 30 April of the simulation year and +# counts 52 weeks or 12 months in a year. For 2026: +# - Carer's Allowance (gov.dwp.carers_allowance.rate, from 2026-04-01): +# 86.45 a week, 86.45 * 52 = 4,495.40; +# - Carer Support Payment (gov.social_security_scotland.carer_support_payment +# .rate, from 2026-04-05): 86.45 a week, 86.45 * 52 = 4,495.40; +# - Scottish Carer Supplement (gov.social_security_scotland +# .carer_support_payment.supplement, from 2026-04-05): 11.70 a week, +# 11.70 * 52 = 608.40; +# - Universal Credit carer element (gov.dwp.universal_credit.elements.carer +# .amount, from 2026-04-01): 209.34 a month, 209.34 * 12 = 2,512.08; +# - Universal Credit standard allowance for a single claimant aged 25 or over +# (gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD, from +# 2026-04-01): 424.90 a month, 424.90 * 12 = 5,098.80; +# - Pension Credit carer addition (gov.dwp.pension_credit.guarantee_credit +# .carer.addition, from 2026-04-01) and the legacy carer premium +# (gov.dwp.carer_premium.single, from 2026-04-01): 48.15 a week each, +# 48.15 * 52 = 2,503.80. +# The minimum caring hours are 35 a week for both benefits +# (gov.dwp.carers_allowance.min_hours and +# gov.social_security_scotland.carer_support_payment.min_hours), so 40 hours +# meet them. + +- name: England carer of a partner with no qualifying disability benefit gets no Carer's Allowance + # The carer cares 40 hours a week for their partner, who has no s.70(2) + # benefit, so the partner is not a severely disabled person and s.70(1) + # gives no allowance: 0 for both. Without a carer benefit or the hours + # route, the carer is not a carer for benefits either. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + cares_for_severely_disabled_person: false + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: ENGLAND + output: + carers_allowance: [0, 0] + is_entitled_to_carer_benefit: [false, false] + is_carer_for_benefits: [false, false] + +- name: England carer with the input left out gets Carer's Allowance on caring hours + # The model takes the person cared for to be severely disabled unless an + # input says otherwise: 40 hours meet the 35-hour minimum, so the carer gets + # the weekly rate all year, 86.45 * 52 = 4,495.40. The partner does not + # care: 0. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: ENGLAND + output: + carers_allowance: [4_495.40, 0] + is_entitled_to_carer_benefit: [true, false] + is_carer_for_benefits: [true, false] + +- name: Scotland carer of a partner with no qualifying disability benefit gets no Carer Support Payment + # Reg 5(1) pays Carer Support Payment only for care of a person to whom a + # qualifying disability benefit is normally payable, and the partner has + # none: 0. The supplement follows a payable Carer Support Payment: 0. From + # 2025 Carer Support Payment replaces Carer's Allowance in Scotland, so + # Carer's Allowance is 0 too. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + cares_for_severely_disabled_person: false + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: SCOTLAND + output: + carer_support_payment: [0, 0] + scottish_carer_supplement: [0, 0] + carers_allowance: [0, 0] + +- name: Scotland carer with the input left out gets Carer Support Payment and the supplement + # 40 hours meet the 35-hour minimum: 86.45 * 52 = 4,495.40 of Carer + # Support Payment and 11.70 * 52 = 608.40 of supplement. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: SCOTLAND + output: + carer_support_payment: [4_495.40, 0] + scottish_carer_supplement: [608.40, 0] + carers_allowance: [0, 0] + +- name: Universal Credit claimant caring for someone with no qualifying disability benefit has no carer element + # The claimant would not claim Carer's Allowance, so no allowance enters + # the award as income; reg 30(2) gives the carer element whether or not the + # claimant claims the allowance. The person cared for is not severely + # disabled, so the claimant fails the Carer's Allowance conditions that + # reg 30(1)(a) applies: no carer element. The award is the standard + # allowance alone, 424.90 * 12 = 5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 40 + would_claim_carers_allowance: false + cares_for_severely_disabled_person: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + carers_allowance: 0 + uc_carer_element: 0 + universal_credit: 5_098.80 + +- name: Universal Credit claimant with the input left out has the carer element + # The same claimant, with the person cared for taken to be severely + # disabled: the carer element, 209.34 * 12 = 2,512.08, and an award of + # 5,098.80 + 2,512.08 = 7,610.88. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 40 + would_claim_carers_allowance: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + carers_allowance: 0 + uc_carer_element: 2_512.08 + universal_credit: 7_610.88 + +- name: A reported Carer's Allowance award counts whatever the input says + # The Department pays a reported award, so the input false leaves it: the + # carer gets the weekly rate all year, 86.45 * 52 = 4,495.40, not the + # reported amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + cares_for_severely_disabled_person: false + carers_allowance_reported: 4_000 + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: ENGLAND + output: + carers_allowance: [4_495.40, 0] + is_entitled_to_carer_benefit: [true, false] + is_carer_for_benefits: [true, false] + +- name: A reported award in Scotland keeps Carer Support Payment whatever the input says + # carers_allowance_reported carries a reported Carer Support Payment award + # too, which Social Security Scotland pays: 86.45 * 52 = 4,495.40 and the + # supplement, 11.70 * 52 = 608.40. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 45 + care_hours: 40 + cares_for_severely_disabled_person: false + carers_allowance_reported: 4_000 + partner: + age: 50 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + country: SCOTLAND + output: + carer_support_payment: [4_495.40, 0] + scottish_carer_supplement: [608.40, 0] + +- name: Couple who both care give the allowance only to the partner who cares for a severely disabled person + # Both care 40 hours a week. The elder, the benefit unit head, cares for + # someone with no qualifying disability benefit, so only the younger meets + # the caring condition and has the allowance, 86.45 * 52 = 4,495.40. With + # no reported or supplied award, the default still treats them as caring + # for the same person, and the elder's hours explain no entitlement, so + # the couple restriction leaves the younger's award alone. One carer + # premium: 48.15 * 52 = 2,503.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 40 + care_hours: 40 + cares_for_severely_disabled_person: false + younger: + age: 38 + care_hours: 40 + benunits: + benunit: + members: [elder, younger] + would_claim_uc: false + households: + household: + members: [elder, younger] + country: ENGLAND + output: + is_claimant_or_partner: [true, true] + partners_care_for_same_severely_disabled_person: true + carers_allowance_pre_couple_limit: [0, 4_495.40] + carers_allowance: [0, 4_495.40] + is_entitled_to_carer_benefit: [false, true] + carer_premium: 2_503.80 + +- name: A supplied entitlement whose caring hours go to someone not severely disabled counts as a second award + # The elder has a reported award. The younger cares 40 hours a week for + # someone with no qualifying disability benefit, and an input supplies the + # younger's entitlement to a carer benefit. The hours can't explain that + # entitlement, so it counts as an award: two awards mean two people cared + # for, and the default is false. Each partner then has the carer premium, + # 2 * 48.15 * 52 = 5,007.60. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 40 + care_hours: 40 + carers_allowance_reported: 4_000 + is_entitled_to_carer_benefit: true + younger: + age: 38 + care_hours: 40 + cares_for_severely_disabled_person: false + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [elder, younger] + would_claim_uc: false + households: + household: + members: [elder, younger] + country: ENGLAND + output: + partners_care_for_same_severely_disabled_person: false + carer_premium: 5_007.60 + +- name: The same couple with the input left out has one carer premium + # Now the younger's 40 hours, for someone the model takes to be severely + # disabled, explain the younger's entitlement, so it is not an award. One + # award leaves the default true, and caring for the same person caps the + # premium at one amount: 48.15 * 52 = 2,503.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 40 + care_hours: 40 + carers_allowance_reported: 4_000 + is_entitled_to_carer_benefit: true + younger: + age: 38 + care_hours: 40 + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [elder, younger] + would_claim_uc: false + households: + household: + members: [elder, younger] + country: ENGLAND + output: + partners_care_for_same_severely_disabled_person: true + carer_premium: 2_503.80 + +- name: Pension-age carer of someone with no qualifying disability benefit gets no Pension Credit carer addition + # A claimant aged 70 cares 40 hours a week for someone outside the + # household who has no qualifying disability benefit. With no entitlement + # to Carer's Allowance, the claimant has neither the Pension Credit carer + # addition nor the legacy carer premium: 0. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + care_hours: 40 + cares_for_severely_disabled_person: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + carers_allowance: 0 + is_entitled_to_carer_benefit: false + carer_minimum_guarantee_addition: 0 + carer_premium: 0 + +- name: Pension-age carer with the input left out gets the Pension Credit carer addition + # The same claimant, entitled to Carer's Allowance on caring hours: 86.45 + # * 52 = 4,495.40 of allowance, and the carer addition and the carer + # premium, 48.15 * 52 = 2,503.80 each. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + care_hours: 40 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + carers_allowance: 4_495.40 + is_entitled_to_carer_benefit: true + carer_minimum_guarantee_addition: 2_503.80 + carer_premium: 2_503.80 diff --git a/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py new file mode 100644 index 0000000000..6d863ed915 --- /dev/null +++ b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py @@ -0,0 +1,382 @@ +"""Carer benefits pay on caring hours only for care of a severely disabled +person. + +SSCBA 1992 s.70(1) entitles a person to Carer's Allowance for caring for a +"severely disabled person", whom s.70(2) defines by the benefit payable for +them. SSI 2023/302 reg 5(1) pays Carer Support Payment only for care of a +person "to whom a qualifying disability benefit is normally payable". UC Regs +2013 reg 30(1)(a) gives the carer element to a person who meets the Carer's +Allowance conditions, or would but for the earnings limit or living in +Scotland. The input cares_for_severely_disabled_person records whether the +person cared for gets such a benefit. It defaults to true and gates only the +route through caring hours: a reported award (carers_allowance_reported) +still counts. + +Invariants, for benefit units of one or two adults and up to one qualifying +young person, each with drawn caring hours, reported award, take-up, +earnings and overlapping benefit, in each country, in 2024 (Scotland still on +Carer's Allowance), 2026, 2030 (the year the Carer's Allowance earnings +taper starts) and 2031: + +- leaving the input out gives exactly the values that setting it true gives, + for carers_allowance, carer_support_payment, is_carer_for_benefits, + is_entitled_to_carer_benefit, uc_carer_element, + partners_care_for_same_severely_disabled_person and + carer_minimum_guarantee_addition; +- setting one person's input false never raises that person's + carers_allowance or carer_support_payment and never turns that person's + is_carer_for_benefits or is_entitled_to_carer_benefit on; it never raises + the benefit unit's uc_carer_element or carer_minimum_guarantee_addition, + and never turns partners_care_for_same_severely_disabled_person on; +- before the taper, setting every person's input false never raises any + person's carers_allowance or carer_support_payment, or the benefit unit's + amounts, and never turns any of those flags on; +- a person whose input is false and who has no reported award gets no + Carer's Allowance or Carer Support Payment, before or after the + overlapping-benefit adjustment, and is neither entitled to a carer benefit + nor a carer for benefits; +- setting the input false for the people with a reported award leaves every + person's carers_allowance and carer_support_payment as they were. + +Partners who care for the same person share one award (SSCBA 1992 +s.70(7ZA); SSI 2023/302 reg 5(3)). So when one partner's input goes false, +the model can give the award to the other partner, whose amount and +entitlement then rise, as the law has it; the second invariant therefore +compares only the person whose input changes, and the benefit unit. Under +the earnings taper the partners' awards can differ, and the restriction gives +the award to the larger: setting both inputs false can then give back a +reported award that the restriction took for the other partner's larger one, +so the third invariant holds only before the taper. The examples below run +both cases. + +The households supply none of the carer benefit amounts or entitlements that +the model also takes as inputs: a value supplied for one person fills every +person in the batched simulation. + +Each example builds every benefit unit twice in one simulation, in separate +households: once as drawn, and once with the input changed. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +INPUT = "cares_for_severely_disabled_person" +YEARS = [2024, 2026, 2030, 2031] +YEARS_BEFORE_TAPER = [2024, 2026] +COUNTRIES = ["ENGLAND", "SCOTLAND", "WALES", "NORTHERN_IRELAND"] +AWARD = 4_000 +# Annual amounts are float32; a change smaller than half a penny is not one. +TOLERANCE = 0.005 + +PERSON_AMOUNTS = ["carers_allowance", "carer_support_payment"] +PERSON_FLAGS = ["is_carer_for_benefits", "is_entitled_to_carer_benefit"] +BENUNIT_AMOUNTS = ["uc_carer_element", "carer_minimum_guarantee_addition"] +BENUNIT_FLAGS = ["partners_care_for_same_severely_disabled_person"] +PRE_OVERLAP = ["carers_allowance_pre_overlap", "carer_support_payment_pre_overlap"] + + +@st.composite +def carer_inputs(draw, earns=True): + # The draws lean towards a claim within the earnings limit, so that most + # carers who care the hours have an award the input can take away. + return { + # Either side of the 35-hour minimum both benefits use. + "care_hours": draw(st.sampled_from([0, 20, 34, 35, 40, 40, 60])), + "carers_allowance_reported": draw(st.sampled_from([0, 0, AWARD])), + "would_claim_carers_allowance": draw(st.sampled_from([True, True, False])), + # Below the earnings limit, above it by less than the rate (so the + # taper pays some allowance), and far above it. + "employment_income": ( + draw(st.sampled_from([0, 0, 0, 8_000, 12_500, 30_000])) if earns else 0 + ), + # Above the carer rates, so an overlapping benefit can reduce the + # payment to nil and leave the entitlement. + "incapacity_benefit_reported": draw(st.sampled_from([0, 0, 6_000])), + } + + +@st.composite +def benefit_units(draw): + """One benefit unit in its own household: its members, the country, one + member whose input a test changes, and for each member the input false + or left out (None).""" + has_young_person = draw(st.booleans()) + young_person_age = draw(st.integers(16, 19)) + head_age = draw(st.integers(young_person_age + 16 if has_young_person else 18, 85)) + members = [{"age": head_age, "is_parent": has_young_person, **draw(carer_inputs())}] + if draw(st.booleans()): + # Within 15 years of the head in both directions when either could be + # under 20, and under 20 years older otherwise, so the model reads + # neither as the other's child. + oldest_partner = min(85, head_age + 19) if head_age >= 20 else head_age + 15 + youngest_partner = max(18, head_age - 15) + if has_young_person: + youngest_partner = max(youngest_partner, young_person_age + 16) + members.append( + { + "age": draw(st.integers(youngest_partner, oldest_partner)), + "is_parent": has_young_person, + **draw(carer_inputs()), + } + ) + adults = len(members) + if has_young_person: + # A qualifying young person: 16 to 19 in non-advanced education. + members.append( + { + "age": young_person_age, + "current_education": "UPPER_SECONDARY", + **draw(carer_inputs(earns=False)), + } + ) + return { + "members": members, + "adults": adults, + "country": draw(st.sampled_from(COUNTRIES)), + "chosen": draw(st.integers(0, len(members) - 1)), + "inputs": [draw(st.sampled_from([False, None])) for _ in members], + } + + +def with_input(unit, values): + """The unit with the input set to each member's value, or left out where + the value is None.""" + members = [ + member if value is None else {**member, INPUT: value} + for member, value in zip(unit["members"], values) + ] + return {**unit, "members": members} + + +def calculate(units, year, variables): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, inputs in enumerate(unit["members"]): + name = f"p{i}_{j}" + people[name] = {key: {year: value} for key, value in inputs.items()} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = {"members": names, "country": {year: unit["country"]}} + sim = Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + values = {name: sim.calculate(name, year) for name in variables} + offsets = np.cumsum([0] + [len(unit["members"]) for unit in units]) + return values, offsets + + +SETTINGS = settings( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + +UNITS = st.lists(benefit_units(), min_size=1, max_size=6) + +ADULT = { + "is_parent": False, + "carers_allowance_reported": 0, + "would_claim_carers_allowance": True, + "employment_income": 0, + "incapacity_benefit_reported": 0, +} +# Both partners care 40 hours a week. The elder, the benefit unit head, has the +# couple's one award; with the elder's input false the younger has it. +COUPLE = { + "members": [ + {**ADULT, "age": 40, "care_hours": 40}, + {**ADULT, "age": 38, "care_hours": 40}, + ], + "adults": 2, + "country": "ENGLAND", + "chosen": 0, + "inputs": [False, None], +} +# The same couple, aged 70 and 60, where an overlapping benefit takes the +# elder's award to nil: with the elder's input false, the younger is paid. +OVERLAPPED_COUPLE = { + **COUPLE, + "members": [ + {**ADULT, "age": 70, "care_hours": 40, "incapacity_benefit_reported": 6_000}, + {**ADULT, "age": 60, "care_hours": 40}, + ], +} +# Under the taper: the elder's reported award, tapered by earnings, loses to +# the younger's larger award on caring hours. With both inputs false the elder +# has it back; with only the elder's false, nothing changes. +TAPERED_COUPLE = { + **COUPLE, + "members": [ + { + **ADULT, + "age": 40, + "care_hours": 40, + "carers_allowance_reported": AWARD, + "employment_income": 12_500, + }, + {**ADULT, "age": 38, "care_hours": 40}, + ], + "inputs": [False, False], +} +SCOTTISH_COUPLE = {**COUPLE, "country": "SCOTLAND"} + + +@SETTINGS +@given(UNITS, st.sampled_from(YEARS)) +@example(drawn=[COUPLE, SCOTTISH_COUPLE], year=2026) +def test_input_left_out_matches_input_true(drawn, year): + variables = PERSON_AMOUNTS + PERSON_FLAGS + ["is_claimant_or_partner"] + benunit_variables = BENUNIT_AMOUNTS + BENUNIT_FLAGS + true = [with_input(unit, [True] * len(unit["members"])) for unit in drawn] + values, offsets = calculate(drawn + true, year, variables + benunit_variables) + k = len(drawn) + n = offsets[k] + for i, unit in enumerate(drawn): + # The generator's construction, checked against the model: the + # adults are the claimant and partner, the young person is not. + flags = values["is_claimant_or_partner"][offsets[i] : offsets[i + 1]] + adults = unit["adults"] + assert flags[:adults].all() and not flags[adults:].any(), unit + for name in variables: + assert np.array_equal(values[name][:n], values[name][n:]), name + for name in benunit_variables: + assert np.array_equal(values[name][:k], values[name][k:]), name + + +@SETTINGS +@given(UNITS, st.sampled_from(YEARS)) +@example(drawn=[COUPLE, OVERLAPPED_COUPLE, SCOTTISH_COUPLE], year=2026) +@example(drawn=[TAPERED_COUPLE], year=2031) +def test_one_persons_input_false_never_raises_their_award(drawn, year): + changed = [ + with_input( + unit, + [ + False if j == unit["chosen"] else None + for j in range(len(unit["members"])) + ], + ) + for unit in drawn + ] + variables = PERSON_AMOUNTS + PERSON_FLAGS + BENUNIT_AMOUNTS + BENUNIT_FLAGS + values, offsets = calculate(drawn + changed, year, variables) + k = len(drawn) + for i, unit in enumerate(drawn): + before = offsets[i] + unit["chosen"] + after = offsets[k + i] + unit["chosen"] + for name in PERSON_AMOUNTS: + assert values[name][after] <= values[name][before] + TOLERANCE, (name, unit) + for name in PERSON_FLAGS: + assert values[name][before] or not values[name][after], (name, unit) + for name in BENUNIT_AMOUNTS: + assert values[name][k + i] <= values[name][i] + TOLERANCE, (name, unit) + for name in BENUNIT_FLAGS: + assert values[name][i] or not values[name][k + i], (name, unit) + + +@SETTINGS +@given(UNITS, st.sampled_from(YEARS_BEFORE_TAPER)) +@example(drawn=[COUPLE, OVERLAPPED_COUPLE, SCOTTISH_COUPLE], year=2026) +def test_everyones_input_false_never_raises_an_award_before_the_taper(drawn, year): + false = [with_input(unit, [False] * len(unit["members"])) for unit in drawn] + variables = PERSON_AMOUNTS + PERSON_FLAGS + BENUNIT_AMOUNTS + BENUNIT_FLAGS + values, offsets = calculate(drawn + false, year, variables) + k = len(drawn) + n = offsets[k] + for name in PERSON_AMOUNTS: + assert (values[name][n:] <= values[name][:n] + TOLERANCE).all(), name + for name in PERSON_FLAGS: + assert not (values[name][n:] & ~values[name][:n]).any(), name + for name in BENUNIT_AMOUNTS: + assert (values[name][k:] <= values[name][:k] + TOLERANCE).all(), name + for name in BENUNIT_FLAGS: + assert not (values[name][k:] & ~values[name][:k]).any(), name + + +@SETTINGS +@given(UNITS, st.sampled_from(YEARS)) +@example(drawn=[COUPLE, TAPERED_COUPLE, SCOTTISH_COUPLE], year=2031) +def test_input_false_without_a_reported_award_pays_no_carer_benefit(drawn, year): + units = [with_input(unit, unit["inputs"]) for unit in drawn] + variables = PERSON_AMOUNTS + PRE_OVERLAP + PERSON_FLAGS + values, offsets = calculate(units, year, variables) + for i, unit in enumerate(drawn): + for j, (member, value) in enumerate(zip(unit["members"], unit["inputs"])): + if value is False and member["carers_allowance_reported"] == 0: + for name in PERSON_AMOUNTS + PRE_OVERLAP: + assert values[name][offsets[i] + j] == 0, (name, unit) + for name in PERSON_FLAGS: + assert not values[name][offsets[i] + j], (name, unit) + + +@SETTINGS +@given(UNITS, st.sampled_from(YEARS)) +@example(drawn=[TAPERED_COUPLE], year=2031) +@example(drawn=[{**COUPLE, "members": TAPERED_COUPLE["members"]}], year=2026) +def test_reported_award_pays_the_same_whatever_the_input(drawn, year): + toggled = [ + with_input( + unit, + [ + False if member["carers_allowance_reported"] > 0 else None + for member in unit["members"] + ], + ) + for unit in drawn + ] + values, offsets = calculate(drawn + toggled, year, PERSON_AMOUNTS) + n = offsets[len(drawn)] + for name in PERSON_AMOUNTS: + assert np.array_equal(values[name][:n], values[name][n:]), name + + +def _couple_values(units, year, variables): + values, _ = calculate(units, year, variables) + return {name: [round(float(x), 2) for x in values[name]] for name in variables} + + +def test_award_passes_to_the_partner_who_cares_for_a_severely_disabled_person(): + # The cases that the second invariant leaves out, run as they are. With + # the elder's input false the younger's allowance rises from nil to the + # full rate, 86.45 * 52 = 4,495.40: one partner's hours no longer count, + # so the other is the only one caring for a severely disabled person. + names = ["carers_allowance", "is_entitled_to_carer_benefit"] + default = _couple_values([COUPLE], 2026, names) + changed = _couple_values([with_input(COUPLE, COUPLE["inputs"])], 2026, names) + assert default == { + "carers_allowance": [4_495.4, 0], + "is_entitled_to_carer_benefit": [1, 0], + } + assert changed == { + "carers_allowance": [0, 4_495.4], + "is_entitled_to_carer_benefit": [0, 1], + } + # Where an overlapping benefit takes the elder's award to nil, the couple + # had nothing and now has the younger's full allowance. + overlapped = _couple_values([OVERLAPPED_COUPLE], 2026, names) + changed = _couple_values( + [with_input(OVERLAPPED_COUPLE, OVERLAPPED_COUPLE["inputs"])], 2026, names + ) + assert overlapped["carers_allowance"] == [0, 0] + assert changed["carers_allowance"] == [0, 4_495.4] + + +def test_under_the_taper_both_inputs_false_can_give_back_a_reported_award(): + # The case that the third invariant leaves out. In 2031 the elder's + # tapered award falls short of the younger's award on caring hours, so + # the couple restriction gives the younger the award. With both inputs + # false the younger has none, and the elder keeps the reported award. + names = ["carers_allowance_pre_couple_limit", "carers_allowance"] + default = _couple_values([TAPERED_COUPLE], 2031, names) + both_false = _couple_values( + [with_input(TAPERED_COUPLE, TAPERED_COUPLE["inputs"])], 2031, names + ) + elder_award, younger_award = default["carers_allowance_pre_couple_limit"] + assert 0 < elder_award < younger_award + assert default["carers_allowance"] == [0, younger_award] + assert both_false["carers_allowance"] == [elder_award, 0] diff --git a/policyengine_uk/utils/carers_allowance_taper.py b/policyengine_uk/utils/carers_allowance_taper.py index e72d9a1de5..c7ea727f3e 100644 --- a/policyengine_uk/utils/carers_allowance_taper.py +++ b/policyengine_uk/utils/carers_allowance_taper.py @@ -45,7 +45,11 @@ def carers_allowance_weekly_by_part_of_year(person, period, parameters): csp_replaces_ca = period.start.year >= 2025 receives_ca = person("carers_allowance_reported", period) > 0 meets_age_condition = person("age", period) >= ca.min_age - meets_work_condition = person("care_hours", period) >= ca.min_hours + # SSCBA 1992 s. 70(1): the hours count only for caring for a severely + # disabled person (s. 70(2)), which the input takes to hold by default. + meets_work_condition = (person("care_hours", period) >= ca.min_hours) & person( + "cares_for_severely_disabled_person", period + ) would_claim = person("would_claim_carers_allowance", period) eligible = ( ~(in_scotland & csp_replaces_ca) diff --git a/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py new file mode 100644 index 0000000000..4211410b8d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py @@ -0,0 +1,59 @@ +from policyengine_uk.model_api import * + + +class cares_for_severely_disabled_person(Variable): + value_type = bool + entity = Person + label = "Cares for a severely disabled person" + documentation = ( + "Whether the person this person cares for, in the household or " + "outside it, gets a benefit that makes them a severely disabled " + "person for Carer's Allowance (SSCBA 1992 s.70(2), with the payments " + "that SI 1976/409 reg 3 prescribes) or a qualifying disability " + "benefit for Carer Support Payment (SSI 2023/302 regs 2 and 5(1)). " + "The Department for Work and Pensions pays Carer's Allowance only for " + "caring for a severely disabled person (s.70(1)), Social Security " + "Scotland pays Carer Support Payment only for care of a person to " + "whom a qualifying disability benefit is normally payable (reg 5(1)), " + "and the Universal Credit carer element needs the Carer's Allowance " + "conditions, apart from the earnings limit and residence in Scotland " + "(UC Regs 2013 reg 30(1)(a)). Caring hours in the survey count care " + "for anyone and don't say whom the carer cares for, and the person " + "cared for can live elsewhere, so the model doesn't derive this from " + "the household's disability benefits: it takes the person cared for " + "to qualify unless an input says otherwise, and the default is true. " + "The model reads it only on the route through caring hours " + "(care_hours at the minimum) to Carer's Allowance, Carer Support " + "Payment and is_carer_for_benefits, and so the Universal Credit " + "carer element, and in the hours that explain an entitlement in " + "partners_care_for_same_severely_disabled_person. A reported award " + "(carers_allowance_reported, the model's reported-receipt input for " + "both benefits) still counts whatever this input says, since the " + "Department or Social Security Scotland pays it. Set it to false for " + "a carer whose caring hours go to someone with none of those " + "benefits." + ) + definition_period = YEAR + default_value = True + reference = [ + dict( + title="Social Security Contributions and Benefits Act 1992 s. 70(1) and (2)", + href="https://www.legislation.gov.uk/ukpga/1992/4/section/70", + ), + dict( + title="The Social Security (Invalid Care Allowance) Regulations 1976 reg. 3", + href="https://www.legislation.gov.uk/uksi/1976/409/regulation/3", + ), + dict( + title="The Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023 reg. 2(2)", + href="https://www.legislation.gov.uk/ssi/2023/302/regulation/2", + ), + dict( + title="The Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023 reg. 5(1)", + href="https://www.legislation.gov.uk/ssi/2023/302/regulation/5", + ), + dict( + title="The Universal Credit Regulations 2013 reg. 30(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/30", + ), + ] diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_unable_to_provide_childcare.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_unable_to_provide_childcare.py index d44a0cfef2..b73e503b6e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_unable_to_provide_childcare.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_unable_to_provide_childcare.py @@ -21,10 +21,12 @@ class uc_unable_to_provide_childcare(Variable): "Limb (ii), defined in regulation 30, reads " "is_carer_for_benefits: receipt of Carer's Allowance or Carer Support " "Payment, or at least the Carer's Allowance qualifying hours of care " - "a week. Its hours branch does not check that the person cared for " - "is severely disabled (a Carer's Allowance condition, SSCBA 1992 " - "s. 70(1)-(2), that regulation 30(1)(a) imports) or exclude a carer " - "paid for the caring (regulation 30(3)). Limb (iii) is the input " + "a week. Its hours branch checks that the person cared for is " + "severely disabled (a Carer's Allowance condition, SSCBA 1992 " + "s. 70(1)-(2), that regulation 30(1)(a) imports) only through the " + "input cares_for_severely_disabled_person, which defaults to true, " + "and does not exclude a carer paid for the caring (regulation " + "30(3)). Limb (iii) is the input " "uc_is_temporarily_absent_from_claimant_household." ) definition_period = YEAR 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 index 7675aabfec..cdc9b5c09d 100644 --- 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 @@ -22,8 +22,9 @@ class meets_uc_minimum_age_condition(Variable): is_carer_for_benefits means receipt of Carer's Allowance or Scottish Carer Support Payment, underlying entitlement to either benefit, 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. + Its hours limb reads the cared-for person's qualifying disability benefit + only from the input cares_for_severely_disabled_person, which defaults to + true, 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 diff --git a/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py b/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py index 775a09b2e4..d491929f34 100644 --- a/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py +++ b/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py @@ -16,6 +16,7 @@ class carer_support_payment_pre_couple_limit(Variable): "https://www.legislation.gov.uk/asp/2018/9/part/4", "https://www.legislation.gov.uk/ssi/2023/302/regulation/3", "https://www.legislation.gov.uk/ssi/2023/302/regulation/4", + "https://www.legislation.gov.uk/ssi/2023/302/regulation/5", "https://www.legislation.gov.uk/ssi/2023/302/regulation/16", "https://www.legislation.gov.uk/ssi/2023/302/regulation/14", "https://www.legislation.gov.uk/ssi/2023/302/schedule/2/paragraph/4", @@ -29,7 +30,12 @@ def formula(person, period, parameters): csp = parameters(period).gov.social_security_scotland.carer_support_payment meets_age_condition = person("age", period) >= csp.min_age weekly_care_hours = person("care_hours", period) - meets_hours = weekly_care_hours >= csp.min_hours + # Reg 5(1): only care of a person to whom a qualifying disability + # benefit is normally payable counts, which the input takes to hold + # by default. + meets_hours = (weekly_care_hours >= csp.min_hours) & person( + "cares_for_severely_disabled_person", period + ) receives_ca = person("carers_allowance_reported", period) > 0 would_claim = person("would_claim_carers_allowance", period) earnings = person("carers_allowance_earnings", period) diff --git a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py index f86e87520d..3c66fdeda7 100644 --- a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py +++ b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py @@ -9,28 +9,39 @@ class is_carer_for_benefits(Variable): documentation = ( "Receives or is entitled to a carer benefit, or provides at least the " - "Carer's Allowance qualifying hours of care a week. The Universal " + "Carer's Allowance qualifying hours of care a week for a severely " + "disabled person. The Universal " "Credit carer element does not require a Carer's Allowance claim. " "Entitlement-based additions and premiums use " "is_entitled_to_carer_benefit; receipt-based rules use " "receives_carer_benefit. A " "carer whose allowance is reduced to nil by an overlapping benefit, " - "such as State Pension, stays entitled. The hours limb tests " - "hours alone, as carers_allowance_pre_overlap does: it does not check that the " - "person cared for receives a qualifying disability benefit (part of " - "the Carer's Allowance conditions referenced by regulation 30), so a " - "survey hours field counts caring for " - "anyone and a dataset's take-up input carries that simplification. " + "such as State Pension, stays entitled. The hours limb, as in " + "carers_allowance_pre_overlap, counts the hours only where the input " + "cares_for_severely_disabled_person holds: the person cared for gets " + "a benefit that makes them a severely disabled person for Carer's " + "Allowance (SSCBA 1992 s.70(1) and (2)), one of the Carer's Allowance " + "conditions that regulation 30(1)(a) applies. The input defaults to " + "true, because caring hours in the survey don't say whom the carer " + "cares for, so at the default a survey hours field counts caring for " + "anyone and a dataset's take-up input carries that simplification. A " + "household calculation sets it false for a carer whose hours go to " + "someone with no such benefit. " "A reported Carer's Allowance award the person would claim also " "counts, so a carer whose earnings put them over the Carer's Allowance " "earnings limit keeps the Universal Credit carer element, which does " "not apply that limit (regulation 30(1)(a)(i))." ) - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/30" + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/30", + "https://www.legislation.gov.uk/ukpga/1992/4/section/70", + ) def formula(person, period, parameters): min_hours = parameters(period).gov.dwp.carers_allowance.min_hours - meets_hours = person("care_hours", period) >= min_hours + meets_hours = (person("care_hours", period) >= min_hours) & person( + "cares_for_severely_disabled_person", period + ) # A reported Carer's Allowance award the person would claim is # evidence of the caring itself, where no hours are recorded. It still # counts when the earnings limit (which the Universal Credit carer diff --git a/policyengine_uk/variables/household/demographic/partners_care_for_same_severely_disabled_person.py b/policyengine_uk/variables/household/demographic/partners_care_for_same_severely_disabled_person.py index 89d137939c..10a6775f8f 100644 --- a/policyengine_uk/variables/household/demographic/partners_care_for_same_severely_disabled_person.py +++ b/policyengine_uk/variables/household/demographic/partners_care_for_same_severely_disabled_person.py @@ -19,19 +19,23 @@ class partners_care_for_same_severely_disabled_person(Variable): "Carer's Allowance or Carer Support Payment award " "(carers_allowance_reported, the model's reported-receipt input for " "both), or an entitlement that caring hours do not explain (fewer " - "than the qualifying hours, or no claim), which must have been " + "than the qualifying hours, care of someone who isn't severely " + "disabled (cares_for_severely_disabled_person false), or no claim), " + "which must have been " "supplied directly (for example as carers_allowance, " "carers_allowance_pre_overlap or is_entitled_to_carer_benefit; supply " "it for every member, since members left out take the default). The " "qualifying hours are Carer Support Payment's in Scotland from 2025 " "and Carer's Allowance's otherwise. Two awards mean two different " "people cared for, while caring hours cannot show who is cared for, " - "so the entitlement of someone who cares the qualifying hours and " - "would claim is not read as an award. " + "so the default doesn't read as an award the entitlement of someone " + "who cares the qualifying hours for a severely disabled person and " + "would claim. " "With more than two members supplied as claimant or partner, " "two awards make the default false for all of them. The model's " "Carer's Allowance and Carer Support Payment candidate amounts follow " - "caring hours or a reported award. The default uses those candidates " + "a reported award or caring hours for a severely disabled person. " + "The default uses those candidates " "and directly supplied awards before the same-person couple limit, " "so it does not depend on the final awards it restricts." ) @@ -48,11 +52,13 @@ def formula(benunit, period, parameters): entitled = unlimited_carer_benefit_entitlement(benunit.members, period) reported_award = benunit.members("carers_allowance_reported", period) > 0 # The model makes someone entitled to a carer benefit only on a - # reported award or the qualifying hours, each for someone who would - # claim, so an entitlement the hours do not explain comes from a - # reported award or was supplied directly (an amount in payment, a - # pre-overlap amount or the entitlement itself). The hours are those - # of the benefit that applies, as in carers_allowance_pre_overlap. + # reported award or the qualifying hours of care for a severely + # disabled person (cares_for_severely_disabled_person), each for + # someone who would claim, so an entitlement the hours do not explain + # comes from a reported award or an input that supplies it directly + # (an amount in payment, a pre-overlap amount or the entitlement + # itself). The hours are those of the benefit that applies, as in + # carers_allowance_pre_overlap. gov = parameters(period).gov in_scotland = ( benunit.members.household("country", period).decode_to_str() == "SCOTLAND" @@ -64,7 +70,12 @@ def formula(benunit, period, parameters): gov.dwp.carers_allowance.min_hours, ) hours = benunit.members("care_hours", period) + cares_for_severely_disabled = benunit.members( + "cares_for_severely_disabled_person", period + ) would_claim = benunit.members("would_claim_carers_allowance", period) - explained_by_hours = (hours >= min_hours) & would_claim + explained_by_hours = ( + (hours >= min_hours) & cares_for_severely_disabled & would_claim + ) award = reported_award | ~explained_by_hours return benunit.sum(claimant_or_partner & entitled & award) < 2 From cdf713a080bd7518c948efe0351bc2b15b7639ca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 11:48:42 +0000 Subject: [PATCH 2/6] Name the actors, fix docs the input makes wrong, test the reported limb Name DWP and Social Security Scotland, not the provisions, as the actors, and attribute the carer element to UC Regs 2013 reg 29 with reg 30(1) defining the caring responsibilities. Say that the model applies the condition in Northern Ireland too. Fix the sentences that said the care-hours interface does not check the person cared for (disability-legacy-benefits.md) and the test comments that listed the reasons caring hours fail to explain an entitlement (carer_premium.yaml) or said is_carer_for_benefits reads only hours. Add a YAML case for a reported award above the earnings limit, which keeps is_carer_for_benefits and the carer element with the input false, and drop an assertion of the same-person default from the couple case, where it changes nothing. Co-Authored-By: Claude Opus 5.5 --- .../gov/dwp/disability-legacy-benefits.md | 3 +- .../finance/benefit/carer_premium.yaml | 5 +- .../uc_childcare_work_condition_reg_32.yaml | 3 +- .../carers_allowance_cared_for_person.yaml | 101 ++++++++++++------ .../utils/carers_allowance_taper.py | 3 +- .../dwp/cares_for_severely_disabled_person.py | 44 +++++--- .../carer_support_payment_pre_couple_limit.py | 4 +- .../demographic/is_carer_for_benefits.py | 10 +- 8 files changed, 116 insertions(+), 57 deletions(-) diff --git a/docs/book/programs/gov/dwp/disability-legacy-benefits.md b/docs/book/programs/gov/dwp/disability-legacy-benefits.md index dc63b96fca..3496743b36 100644 --- a/docs/book/programs/gov/dwp/disability-legacy-benefits.md +++ b/docs/book/programs/gov/dwp/disability-legacy-benefits.md @@ -94,7 +94,8 @@ annual netting does not represent benefit spells at different weekly rates. The focused household tests retain live UC eligibility, disability/carer elements and final awards. Legacy receipt and disability-assessment inputs are assumptions, not a re-derivation of IB/SDA entitlement. The care-hours interface -does not identify the person cared for or check their qualifying benefit. No +does not identify the person cared for, and reads their qualifying benefit only +from the input `cares_for_severely_disabled_person`, which defaults to true. No private-data impact or Axiom parity has been measured. ## References diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml index 63eb07b0a2..851d941389 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml @@ -19,8 +19,9 @@ # partners are treated as caring for the same person unless both are entitled # with an award: a reported Carer's Allowance or Carer Support Payment award # (carers_allowance_reported carries both), or an entitlement that caring -# hours do not explain (fewer than 35 a week, or no claim), which was supplied -# directly. Most cases below with two premiums therefore give both partners a +# hours do not explain (fewer than 35 a week, care of someone who isn't +# severely disabled (cares_for_severely_disabled_person false), or no claim), +# which was supplied directly. Most cases below with two premiums therefore give both partners a # reported award. - name: Carer premium for a single claimant entitled to Carer's Allowance diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml index 6add9f640f..0aee9af7f4 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml @@ -128,7 +128,8 @@ # allowance, so is a "severely disabled person" (SSCBA 1992 s. 70(2)). # Sara meets the Carer's Allowance conditions that reg. 30(1)(a) # imports, so "both Ben and Sara satisfy the paid work test" (ADM F7013). - # The model's is_carer_for_benefits reads only Sara's 40 care hours; it + # The model's is_carer_for_benefits reads Sara's 40 care hours and her + # cares_for_severely_disabled_person, left at its default of true; it # does not check her mother's attendance allowance. is_carer_for_benefits: [false, true, false, false] uc_childcare_work_condition: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml index f676012ff8..f79eee0ebf 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_cared_for_person.yaml @@ -1,5 +1,5 @@ # Carer benefits for caring for a severely disabled person. -# SSCBA 1992 s.70(1): "A person shall be entitled to a carer's allowance for +# SSCBA 1992 s.70(1): "A person shall be entitled to a carer’s allowance for # any day on which he is engaged in caring for a severely disabled person". # s.70(2) defines a "severely disabled person" by the benefit payable for them: # attendance allowance, the disability living allowance care component at the @@ -9,16 +9,18 @@ # SSI 2023/302 reg 5(1): "Carer Support Payment may only be paid to an # individual in respect of a period during which that individual provides # regular and substantial care to a person to whom a qualifying disability -# benefit is normally payable". UC Regs 2013 reg 30(1)(a) gives the carer -# element to a person who "satisfy the conditions for entitlement to a -# carer's allowance", or would but for the earnings limit or residence in -# Scotland. +# benefit is normally payable". DWP includes the Universal Credit carer +# element (UC Regs 2013 reg 29) for a claimant with regular and substantial +# caring responsibilities for a severely disabled person, which a claimant +# has if "they satisfy the conditions for entitlement to a carer’s +# allowance", or would but for the earnings limit or residence in Scotland +# (reg 30(1)(a)), whether or not they claim the allowance (reg 30(2)). # # cares_for_severely_disabled_person records whether the person cared for # gets such a benefit. It defaults to true. False removes the route through # caring hours; a reported award (carers_allowance_reported) still counts. # -# The model reads each parameter at 30 April of the simulation year and +# The model reads these parameters at 30 April of the simulation year and # counts 52 weeks or 12 months in a year. For 2026: # - Carer's Allowance (gov.dwp.carers_allowance.rate, from 2026-04-01): # 86.45 a week, 86.45 * 52 = 4,495.40; @@ -43,8 +45,8 @@ - name: England carer of a partner with no qualifying disability benefit gets no Carer's Allowance # The carer cares 40 hours a week for their partner, who has no s.70(2) - # benefit, so the partner is not a severely disabled person and s.70(1) - # gives no allowance: 0 for both. Without a carer benefit or the hours + # benefit, so the partner is not a severely disabled person and DWP pays + # no allowance (s.70(1)): 0 for both. Without a carer benefit or the hours # route, the carer is not a carer for benefits either. period: 2026 absolute_error_margin: 0.01 @@ -95,11 +97,11 @@ is_carer_for_benefits: [true, false] - name: Scotland carer of a partner with no qualifying disability benefit gets no Carer Support Payment - # Reg 5(1) pays Carer Support Payment only for care of a person to whom a - # qualifying disability benefit is normally payable, and the partner has - # none: 0. The supplement follows a payable Carer Support Payment: 0. From - # 2025 Carer Support Payment replaces Carer's Allowance in Scotland, so - # Carer's Allowance is 0 too. + # Social Security Scotland pays Carer Support Payment only for care of a + # person to whom a qualifying disability benefit is normally payable (reg + # 5(1)), and the partner has none: 0. The model pays the supplement only + # with a payable Carer Support Payment: 0. From 2025 Carer Support Payment + # replaces Carer's Allowance in Scotland, so Carer's Allowance is 0 too. period: 2026 absolute_error_margin: 0.01 input: @@ -148,11 +150,10 @@ - name: Universal Credit claimant caring for someone with no qualifying disability benefit has no carer element # The claimant would not claim Carer's Allowance, so no allowance enters - # the award as income; reg 30(2) gives the carer element whether or not the - # claimant claims the allowance. The person cared for is not severely - # disabled, so the claimant fails the Carer's Allowance conditions that - # reg 30(1)(a) applies: no carer element. The award is the standard - # allowance alone, 424.90 * 12 = 5,098.80. + # the award as income, and the carer element needs no claim (reg 30(2)). + # The person cared for is not severely disabled, so the claimant fails the + # Carer's Allowance conditions in reg 30(1)(a): no carer element. The award + # is the standard allowance alone, 424.90 * 12 = 5,098.80. period: 2026 absolute_error_margin: 0.01 input: @@ -199,9 +200,9 @@ universal_credit: 7_610.88 - name: A reported Carer's Allowance award counts whatever the input says - # The Department pays a reported award, so the input false leaves it: the - # carer gets the weekly rate all year, 86.45 * 52 = 4,495.40, not the - # reported amount. + # The Department for Work and Pensions pays a reported award, so the input + # false leaves it: the carer gets the weekly rate all year, 86.45 * 52 = + # 4,495.40, not the reported amount. period: 2026 absolute_error_margin: 0.01 input: @@ -251,14 +252,53 @@ carer_support_payment: [4_495.40, 0] scottish_carer_supplement: [608.40, 0] +- name: A reported award above the earnings limit keeps the carer element whatever the input says + # The claimant reports a Carer's Allowance award but earns 20,000 a year. + # Income tax takes 20% (gov.hmrc.income_tax.rates.uk) of 20,000 less the + # 12,570 personal allowance, 0.2 * 7,430 = 1,486, and employee National + # Insurance takes 8% (gov.hmrc.national_insurance.class_1.rates.employee + # .main) above the primary threshold of 241.73 a week (12,569.96 a year), + # 0.08 * 7,430.04 = 594.40. Net earnings come to (20,000 - 1,486 - 594.40) + # / 52 = 344.61 a week, above the 204 earnings limit + # (gov.dwp.carers_allowance.earnings_limit, from 2026-04-06), so the + # claimant is gainfully employed and DWP pays no allowance (s.70(1)(b)): + # 0. The reported award still counts as caring for a severely disabled + # person whatever the input says, and a claimant over the earnings limit + # still has the caring responsibilities the carer element needs (reg + # 30(1)(a)(i)), so the claimant is a carer for benefits and the element is + # 209.34 * 12 = 2,512.08. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 40 + employment_income: 20_000 + cares_for_severely_disabled_person: false + carers_allowance_reported: 4_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + is_entitled_to_carer_benefit: false + is_carer_for_benefits: true + uc_carer_element: 2_512.08 + - name: Couple who both care give the allowance only to the partner who cares for a severely disabled person # Both care 40 hours a week. The elder, the benefit unit head, cares for # someone with no qualifying disability benefit, so only the younger meets # the caring condition and has the allowance, 86.45 * 52 = 4,495.40. With - # no reported or supplied award, the default still treats them as caring - # for the same person, and the elder's hours explain no entitlement, so - # the couple restriction leaves the younger's award alone. One carer - # premium: 48.15 * 52 = 2,503.80. + # the input left out the elder would have the couple's one award. With one + # partner entitled, the couple restriction has no choice to make, whatever + # partners_care_for_same_severely_disabled_person says. One carer premium: + # 48.15 * 52 = 2,503.80. period: 2026 absolute_error_margin: 0.01 input: @@ -280,7 +320,6 @@ country: ENGLAND output: is_claimant_or_partner: [true, true] - partners_care_for_same_severely_disabled_person: true carers_allowance_pre_couple_limit: [0, 4_495.40] carers_allowance: [0, 4_495.40] is_entitled_to_carer_benefit: [false, true] @@ -320,10 +359,12 @@ carer_premium: 5_007.60 - name: The same couple with the input left out has one carer premium - # Now the younger's 40 hours, for someone the model takes to be severely - # disabled, explain the younger's entitlement, so it is not an award. One - # award leaves the default true, and caring for the same person caps the - # premium at one amount: 48.15 * 52 = 2,503.80. + # The model's same-person default reads an entitlement that caring hours + # explain as no award, since hours can't show whom a carer cares for. Now + # the younger's 40 hours, for someone the model takes to be severely + # disabled, explain the younger's entitlement. One award leaves the + # default true, and caring for the same person caps the premium at one + # amount: 48.15 * 52 = 2,503.80. period: 2026 absolute_error_margin: 0.01 input: diff --git a/policyengine_uk/utils/carers_allowance_taper.py b/policyengine_uk/utils/carers_allowance_taper.py index c7ea727f3e..ef7d5ecd7c 100644 --- a/policyengine_uk/utils/carers_allowance_taper.py +++ b/policyengine_uk/utils/carers_allowance_taper.py @@ -46,7 +46,8 @@ def carers_allowance_weekly_by_part_of_year(person, period, parameters): receives_ca = person("carers_allowance_reported", period) > 0 meets_age_condition = person("age", period) >= ca.min_age # SSCBA 1992 s. 70(1): the hours count only for caring for a severely - # disabled person (s. 70(2)), which the input takes to hold by default. + # disabled person (s. 70(2)), which the model takes to hold unless the + # input says otherwise. meets_work_condition = (person("care_hours", period) >= ca.min_hours) & person( "cares_for_severely_disabled_person", period ) diff --git a/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py index 4211410b8d..fba05bc9a4 100644 --- a/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py +++ b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py @@ -12,24 +12,30 @@ class cares_for_severely_disabled_person(Variable): "that SI 1976/409 reg 3 prescribes) or a qualifying disability " "benefit for Carer Support Payment (SSI 2023/302 regs 2 and 5(1)). " "The Department for Work and Pensions pays Carer's Allowance only for " - "caring for a severely disabled person (s.70(1)), Social Security " + "caring for a severely disabled person (s.70(1)), and the model " + "applies the same condition in Northern Ireland. Social Security " "Scotland pays Carer Support Payment only for care of a person to " - "whom a qualifying disability benefit is normally payable (reg 5(1)), " - "and the Universal Credit carer element needs the Carer's Allowance " - "conditions, apart from the earnings limit and residence in Scotland " - "(UC Regs 2013 reg 30(1)(a)). Caring hours in the survey count care " - "for anyone and don't say whom the carer cares for, and the person " - "cared for can live elsewhere, so the model doesn't derive this from " - "the household's disability benefits: it takes the person cared for " - "to qualify unless an input says otherwise, and the default is true. " - "The model reads it only on the route through caring hours " - "(care_hours at the minimum) to Carer's Allowance, Carer Support " - "Payment and is_carer_for_benefits, and so the Universal Credit " - "carer element, and in the hours that explain an entitlement in " - "partners_care_for_same_severely_disabled_person. A reported award " + "whom a qualifying disability benefit is normally payable (reg 5(1)). " + "DWP includes the Universal Credit carer element (UC Regs 2013 reg " + "29) for a claimant with regular and substantial caring " + "responsibilities for a severely disabled person: one who meets the " + "Carer's Allowance conditions, or would but for the earnings limit or " + "residence in Scotland, or who has an entitlement to Carer Support " + "Payment (reg 30(1)). Caring hours in the survey " + "count care for anyone and don't say whom the carer cares for, and " + "the person cared for can live elsewhere, so the model doesn't derive " + "this from the household's disability benefits: it takes the person " + "cared for to qualify unless this input says otherwise, and the " + "default is true. The model reads it only where care_hours reach the " + "minimum: on the route through caring hours to Carer's Allowance, " + "Carer Support Payment and is_carer_for_benefits (and so the " + "Universal Credit carer element), and in " + "partners_care_for_same_severely_disabled_person, which counts a " + "supplied entitlement as a separate award unless caring hours for a " + "severely disabled person explain it. A reported award " "(carers_allowance_reported, the model's reported-receipt input for " "both benefits) still counts whatever this input says, since the " - "Department or Social Security Scotland pays it. Set it to false for " + "department or Social Security Scotland pays it. Set it to false for " "a carer whose caring hours go to someone with none of those " "benefits." ) @@ -40,6 +46,10 @@ class cares_for_severely_disabled_person(Variable): title="Social Security Contributions and Benefits Act 1992 s. 70(1) and (2)", href="https://www.legislation.gov.uk/ukpga/1992/4/section/70", ), + dict( + title="Social Security Contributions and Benefits (Northern Ireland) Act 1992 s. 70", + href="https://www.legislation.gov.uk/ukpga/1992/7/section/70", + ), dict( title="The Social Security (Invalid Care Allowance) Regulations 1976 reg. 3", href="https://www.legislation.gov.uk/uksi/1976/409/regulation/3", @@ -52,6 +62,10 @@ class cares_for_severely_disabled_person(Variable): title="The Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023 reg. 5(1)", href="https://www.legislation.gov.uk/ssi/2023/302/regulation/5", ), + dict( + title="The Universal Credit Regulations 2013 reg. 29", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/29", + ), dict( title="The Universal Credit Regulations 2013 reg. 30(1)(a)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/30", diff --git a/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py b/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py index d491929f34..80aa3b657c 100644 --- a/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py +++ b/policyengine_uk/variables/gov/social_security_scotland/carer_support_payment_pre_couple_limit.py @@ -31,8 +31,8 @@ def formula(person, period, parameters): meets_age_condition = person("age", period) >= csp.min_age weekly_care_hours = person("care_hours", period) # Reg 5(1): only care of a person to whom a qualifying disability - # benefit is normally payable counts, which the input takes to hold - # by default. + # benefit is normally payable counts, which the model takes to hold + # unless the input says otherwise. meets_hours = (weekly_care_hours >= csp.min_hours) & person( "cares_for_severely_disabled_person", period ) diff --git a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py index 3c66fdeda7..7a01840354 100644 --- a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py +++ b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py @@ -21,11 +21,11 @@ class is_carer_for_benefits(Variable): "cares_for_severely_disabled_person holds: the person cared for gets " "a benefit that makes them a severely disabled person for Carer's " "Allowance (SSCBA 1992 s.70(1) and (2)), one of the Carer's Allowance " - "conditions that regulation 30(1)(a) applies. The input defaults to " - "true, because caring hours in the survey don't say whom the carer " - "cares for, so at the default a survey hours field counts caring for " - "anyone and a dataset's take-up input carries that simplification. A " - "household calculation sets it false for a carer whose hours go to " + "conditions in regulation 30(1)(a). The input defaults to true, " + "because caring hours in the survey don't say whom the carer cares " + "for, so at the default a survey hours field counts caring for anyone " + "and a dataset's take-up input carries that simplification. Set it to " + "false in a household calculation for a carer whose hours go to " "someone with no such benefit. " "A reported Carer's Allowance award the person would claim also " "counts, so a carer whose earnings put them over the Carer's Allowance " From b39075893365a6b6d52fb4bcc03ef52160d42375 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 11:56:09 +0000 Subject: [PATCH 3/6] Test that a supplied entitlement counts as an award once hours stop counting Without a supplied entitlement the input can't change partners_care_for_same_severely_disabled_person, so the properties gave no evidence about explained_by_hours. Add a property over couples whose entitlements an input supplies, true or false, for every person: the flag is false exactly where both partners are entitled and have an award that caring hours for a severely disabled person don't explain. State the scope of the other invariants, name the actors in the module docstring, and raise the tapered earner to 13,500 so the 2031 taper still pays a partial award if the national living wage projection moves. Co-Authored-By: Claude Opus 5.5 --- .../finance/benefit/carer_premium.yaml | 2 +- ...for_severely_disabled_person_properties.py | 220 +++++++++++++----- 2 files changed, 166 insertions(+), 56 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml index 851d941389..4d508cec72 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml @@ -21,7 +21,7 @@ # (carers_allowance_reported carries both), or an entitlement that caring # hours do not explain (fewer than 35 a week, care of someone who isn't # severely disabled (cares_for_severely_disabled_person false), or no claim), -# which was supplied directly. Most cases below with two premiums therefore give both partners a +# which an input supplied directly. Most cases below with two premiums therefore give both partners a # reported award. - name: Carer premium for a single claimant entitled to Carer's Allowance diff --git a/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py index 6d863ed915..d7668af6e9 100644 --- a/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py +++ b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py @@ -1,60 +1,80 @@ """Carer benefits pay on caring hours only for care of a severely disabled person. -SSCBA 1992 s.70(1) entitles a person to Carer's Allowance for caring for a -"severely disabled person", whom s.70(2) defines by the benefit payable for -them. SSI 2023/302 reg 5(1) pays Carer Support Payment only for care of a -person "to whom a qualifying disability benefit is normally payable". UC Regs -2013 reg 30(1)(a) gives the carer element to a person who meets the Carer's +The Department for Work and Pensions pays Carer's Allowance only for caring +for a "severely disabled person" (SSCBA 1992 s.70(1)), defined by the benefit +payable for them (s.70(2)). Social Security Scotland pays Carer Support +Payment only for care of a person "to whom a qualifying disability benefit is +normally payable" (SSI 2023/302 reg 5(1)). DWP includes the Universal Credit +carer element (UC Regs 2013 reg 29) for a claimant who meets the Carer's Allowance conditions, or would but for the earnings limit or living in -Scotland. The input cares_for_severely_disabled_person records whether the -person cared for gets such a benefit. It defaults to true and gates only the -route through caring hours: a reported award (carers_allowance_reported) -still counts. +Scotland (reg 30(1)(a)). The input cares_for_severely_disabled_person records +whether the person cared for gets such a benefit. It defaults to true, and +the model reads it only on the route through caring hours: a reported award +(carers_allowance_reported) still counts. Invariants, for benefit units of one or two adults and up to one qualifying young person, each with drawn caring hours, reported award, take-up, earnings and overlapping benefit, in each country, in 2024 (Scotland still on Carer's Allowance), 2026, 2030 (the year the Carer's Allowance earnings -taper starts) and 2031: - -- leaving the input out gives exactly the values that setting it true gives, - for carers_allowance, carer_support_payment, is_carer_for_benefits, - is_entitled_to_carer_benefit, uc_carer_element, - partners_care_for_same_severely_disabled_person and - carer_minimum_guarantee_addition; -- setting one person's input false never raises that person's - carers_allowance or carer_support_payment and never turns that person's - is_carer_for_benefits or is_entitled_to_carer_benefit on; it never raises - the benefit unit's uc_carer_element or carer_minimum_guarantee_addition, - and never turns partners_care_for_same_severely_disabled_person on; -- before the taper, setting every person's input false never raises any - person's carers_allowance or carer_support_payment, or the benefit unit's - amounts, and never turns any of those flags on; -- a person whose input is false and who has no reported award gets no - Carer's Allowance or Carer Support Payment, before or after the - overlapping-benefit adjustment, and is neither entitled to a carer benefit - nor a carer for benefits; -- setting the input false for the people with a reported award leaves every - person's carers_allowance and carer_support_payment as they were. - -Partners who care for the same person share one award (SSCBA 1992 -s.70(7ZA); SSI 2023/302 reg 5(3)). So when one partner's input goes false, -the model can give the award to the other partner, whose amount and -entitlement then rise, as the law has it; the second invariant therefore +taper starts) and 2031, where no input supplies a carer benefit amount or +entitlement: + +1. leaving the input out gives exactly the values that setting it true + gives, for carers_allowance, carer_support_payment, is_carer_for_benefits, + is_entitled_to_carer_benefit, uc_carer_element, + partners_care_for_same_severely_disabled_person and + carer_minimum_guarantee_addition; +2. setting one person's input false never raises that person's + carers_allowance or carer_support_payment and never turns that person's + is_carer_for_benefits or is_entitled_to_carer_benefit on; it never raises + the benefit unit's uc_carer_element or carer_minimum_guarantee_addition, + and never turns partners_care_for_same_severely_disabled_person on; +3. before the taper, setting every person's input false never raises any + person's carers_allowance or carer_support_payment, or the benefit unit's + amounts, and never turns any of those flags on; +4. a person whose input is false and who has no reported award gets no + Carer's Allowance or Carer Support Payment, before or after the + overlapping-benefit adjustment, and is neither entitled to a carer benefit + nor a carer for benefits; +5. setting the input false for the people with a reported award leaves every + person's carers_allowance and carer_support_payment as they were. + +And for couples where an input supplies each partner's entitlement to a +carer benefit, true or false: + +6. partners_care_for_same_severely_disabled_person is false exactly where + both partners have an entitlement and an award: a reported award, or an + entitlement that caring hours for a severely disabled person don't explain + (fewer than the minimum hours, the input false, or no claim). Leaving the + input out gives what setting it true gives, and setting inputs false never + turns the flag on. + +Only one of two partners who care for the same person can have the award +(SSCBA 1992 s.70(7ZA); SSI 2023/302 reg 5(3)). So when one partner's input +goes false, the model can give the award to the other partner, whose amount +and entitlement then rise, as the law has it; the second invariant therefore compares only the person whose input changes, and the benefit unit. Under -the earnings taper the partners' awards can differ, and the restriction gives -the award to the larger: setting both inputs false can then give back a -reported award that the restriction took for the other partner's larger one, -so the third invariant holds only before the taper. The examples below run -both cases. - -The households supply none of the carer benefit amounts or entitlements that -the model also takes as inputs: a value supplied for one person fills every -person in the batched simulation. - -Each example builds every benefit unit twice in one simulation, in separate -households: once as drawn, and once with the input changed. +the earnings taper the partners' awards can differ, and the model gives the +award to the partner with the larger one: setting both inputs false can then +give the other partner back a reported award, so the third invariant holds +only before the taper. Two fixed tests at the end run both cases. + +The first five invariants need households where no input supplies a carer +benefit entitlement or amount. Without one, the input can't change +partners_care_for_same_severely_disabled_person, so the sixth invariant +supplies the entitlements. A supplied entitlement that caring hours no longer +explain counts as a second award: it can turn that flag false and give the +couple a second carer premium or Universal Credit carer element, as the YAML +case "A supplied entitlement whose caring hours go to someone not severely +disabled counts as a second award" shows. Batching needs the same care: +supplying a value for one person makes it an input for every person in the +simulation, so the others take its default, not the formula. The sixth +invariant's households supply the entitlement for every person. + +Tests 1, 2, 3, 5 and 6 build each benefit unit more than once in one +simulation, in separate households: as drawn, and with the input changed. +Test 4 builds each unit once, with each member's input false or left out. """ import numpy as np @@ -87,10 +107,12 @@ def carer_inputs(draw, earns=True): "care_hours": draw(st.sampled_from([0, 20, 34, 35, 40, 40, 60])), "carers_allowance_reported": draw(st.sampled_from([0, 0, AWARD])), "would_claim_carers_allowance": draw(st.sampled_from([True, True, False])), - # Below the earnings limit, above it by less than the rate (so the - # taper pays some allowance), and far above it. + # From 2026: below the earnings limit, above it by less than the rate + # (13,500 exceeds it by 50.60 a week in 2026 and 21.70 in 2031, so the + # taper pays some allowance), and far above it. The lower 2024 limit + # puts 8,000 and 13,500 above it, by 2.84 and 103.60 a week. "employment_income": ( - draw(st.sampled_from([0, 0, 0, 8_000, 12_500, 30_000])) if earns else 0 + draw(st.sampled_from([0, 0, 0, 8_000, 13_500, 30_000])) if earns else 0 ), # Above the carer rates, so an overlapping benefit can reduce the # payment to nil and leave the entitlement. @@ -151,7 +173,33 @@ def with_input(unit, values): return {**unit, "members": members} -def calculate(units, year, variables): +@st.composite +def couples_with_supplied_entitlements(draw): + """A couple in its own household, where an input supplies each partner's + entitlement to a carer benefit, true or false, and each partner's input + is false or left out (None).""" + head_age = draw(st.integers(18, 85)) + oldest_partner = min(85, head_age + 19) if head_age >= 20 else head_age + 15 + ages = [head_age, draw(st.integers(max(18, head_age - 15), oldest_partner))] + members = [ + { + "age": age, + "is_parent": False, + "is_entitled_to_carer_benefit": draw(st.booleans()), + **draw(carer_inputs()), + } + for age in ages + ] + return { + "members": members, + "adults": 2, + "country": draw(st.sampled_from(COUNTRIES)), + "chosen": draw(st.integers(0, 1)), + "inputs": [draw(st.sampled_from([False, None])) for _ in members], + } + + +def simulate(units, year): people, benunits, households = {}, {}, {} for i, unit in enumerate(units): names = [] @@ -164,8 +212,13 @@ def calculate(units, year, variables): sim = Simulation( situation={"people": people, "benunits": benunits, "households": households} ) - values = {name: sim.calculate(name, year) for name in variables} offsets = np.cumsum([0] + [len(unit["members"]) for unit in units]) + return sim, offsets + + +def calculate(units, year, variables): + sim, offsets = simulate(units, year) + values = {name: sim.calculate(name, year) for name in variables} return values, offsets @@ -217,13 +270,32 @@ def calculate(units, year, variables): "age": 40, "care_hours": 40, "carers_allowance_reported": AWARD, - "employment_income": 12_500, + "employment_income": 13_500, }, {**ADULT, "age": 38, "care_hours": 40}, ], "inputs": [False, False], } SCOTTISH_COUPLE = {**COUPLE, "country": "SCOTLAND"} +# The elder has a reported award; the younger cares 40 hours a week for +# someone not severely disabled. Each has a supplied entitlement, so the +# younger's counts as a second award once the input says the hours don't +# explain it. +SUPPLIED_COUPLE = { + **COUPLE, + "members": [ + { + **ADULT, + "age": 40, + "care_hours": 40, + "carers_allowance_reported": AWARD, + "is_entitled_to_carer_benefit": True, + }, + {**ADULT, "age": 38, "care_hours": 40, "is_entitled_to_carer_benefit": True}, + ], + "chosen": 1, + "inputs": [None, False], +} @SETTINGS @@ -335,6 +407,44 @@ def test_reported_award_pays_the_same_whatever_the_input(drawn, year): assert np.array_equal(values[name][:n], values[name][n:]), name +@SETTINGS +@given( + st.lists(couples_with_supplied_entitlements(), min_size=1, max_size=6), + st.sampled_from(YEARS), +) +@example(drawn=[SUPPLIED_COUPLE, {**SUPPLIED_COUPLE, "country": "SCOTLAND"}], year=2026) +def test_a_supplied_entitlement_counts_as_an_award_unless_hours_explain_it(drawn, year): + true = [with_input(unit, [True, True]) for unit in drawn] + changed = [with_input(unit, unit["inputs"]) for unit in drawn] + sim, offsets = simulate(drawn + true + changed, year) + same = sim.calculate("partners_care_for_same_severely_disabled_person", year) + claimant_or_partner = sim.calculate("is_claimant_or_partner", year) + gov = sim.tax_benefit_system.parameters(year).gov + k = len(drawn) + assert claimant_or_partner.all() + for i, unit in enumerate(drawn): + assert same[k + i] == same[i], unit + assert same[i] or not same[2 * k + i], unit + # The minimum hours of the benefit that applies: Carer Support + # Payment's in Scotland from 2025, Carer's Allowance's otherwise. + if unit["country"] == "SCOTLAND" and year >= 2025: + min_hours = gov.social_security_scotland.carer_support_payment.min_hours + else: + min_hours = gov.dwp.carers_allowance.min_hours + awards = 0 + for member, value in zip(unit["members"], unit["inputs"]): + explained = ( + member["care_hours"] >= min_hours + and value is not False + and member["would_claim_carers_allowance"] + ) + reported = member["carers_allowance_reported"] > 0 + awards += member["is_entitled_to_carer_benefit"] and ( + reported or not explained + ) + assert same[2 * k + i] == (awards < 2), unit + + def _couple_values(units, year, variables): values, _ = calculate(units, year, variables) return {name: [round(float(x), 2) for x in values[name]] for name in variables} @@ -369,8 +479,8 @@ def test_award_passes_to_the_partner_who_cares_for_a_severely_disabled_person(): def test_under_the_taper_both_inputs_false_can_give_back_a_reported_award(): # The case that the third invariant leaves out. In 2031 the elder's # tapered award falls short of the younger's award on caring hours, so - # the couple restriction gives the younger the award. With both inputs - # false the younger has none, and the elder keeps the reported award. + # the model gives the younger the couple's award. With both inputs false + # the younger has none, and the elder keeps the reported award. names = ["carers_allowance_pre_couple_limit", "carers_allowance"] default = _couple_values([TAPERED_COUPLE], 2031, names) both_false = _couple_values( From b60b686e9e5d107a1ff10938d6b29a1fe79820e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:03:01 +0000 Subject: [PATCH 4/6] Rewrap the carer premium test header Co-Authored-By: Claude Opus 5.5 --- .../tests/policy/baseline/finance/benefit/carer_premium.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml index 4d508cec72..0873c9466c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/carer_premium.yaml @@ -21,8 +21,8 @@ # (carers_allowance_reported carries both), or an entitlement that caring # hours do not explain (fewer than 35 a week, care of someone who isn't # severely disabled (cares_for_severely_disabled_person false), or no claim), -# which an input supplied directly. Most cases below with two premiums therefore give both partners a -# reported award. +# which an input supplied directly. Most cases below with two premiums +# therefore give both partners a reported award. - name: Carer premium for a single claimant entitled to Carer's Allowance period: 2026 From 1e1d87e4fd3d9394f0299716b924e5d92179811e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:44:04 +0000 Subject: [PATCH 5/6] Check the reported limb of is_carer_for_benefits in the properties Setting the input false for people with a reported award now also leaves is_carer_for_benefits as it was, except for a reported recipient who would not claim. A mutant that gates the reported limb with the input passed every property before; this assertion fails on it. Co-Authored-By: Claude Opus 5.5 --- ...for_severely_disabled_person_properties.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py index d7668af6e9..2a5ba00304 100644 --- a/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py +++ b/policyengine_uk/tests/test_cares_for_severely_disabled_person_properties.py @@ -38,7 +38,9 @@ overlapping-benefit adjustment, and is neither entitled to a carer benefit nor a carer for benefits; 5. setting the input false for the people with a reported award leaves every - person's carers_allowance and carer_support_payment as they were. + person's carers_allowance and carer_support_payment as they were, and + every person's is_carer_for_benefits too, except for a reported recipient + who would not claim, whose route through caring hours the input closes. And for couples where an input supplies each partner's entitlement to a carer benefit, true or false: @@ -401,10 +403,24 @@ def test_reported_award_pays_the_same_whatever_the_input(drawn, year): ) for unit in drawn ] - values, offsets = calculate(drawn + toggled, year, PERSON_AMOUNTS) + variables = PERSON_AMOUNTS + ["is_carer_for_benefits"] + values, offsets = calculate(drawn + toggled, year, variables) n = offsets[len(drawn)] for name in PERSON_AMOUNTS: assert np.array_equal(values[name][:n], values[name][n:]), name + # A reported award the person would claim keeps them a carer for benefits + # whatever the input says, as the reported limb of is_carer_for_benefits + # has it; a reported recipient who would not claim relies on the hours. + claims = np.array( + [ + member["carers_allowance_reported"] == 0 + or member["would_claim_carers_allowance"] + for unit in drawn + for member in unit["members"] + ] + ) + carer = values["is_carer_for_benefits"] + assert np.array_equal(carer[:n][claims], carer[n:][claims]) @SETTINGS From be8c090a5e8c3ef184169becbd24976ba54ebd51 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 4 Nov 2026 08:26:27 -0500 Subject: [PATCH 6/6] Cite SSI 2023/302 regulation 2, which has no numbered paragraphs The Scottish Ministers list the qualifying disability benefits in regulation 2 of the Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023, whose definitions carry no paragraph numbers. The link already points at regulation 2. Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/dwp/cares_for_severely_disabled_person.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py index fba05bc9a4..d3699861a3 100644 --- a/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py +++ b/policyengine_uk/variables/gov/dwp/cares_for_severely_disabled_person.py @@ -55,7 +55,7 @@ class cares_for_severely_disabled_person(Variable): href="https://www.legislation.gov.uk/uksi/1976/409/regulation/3", ), dict( - title="The Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023 reg. 2(2)", + title="The Carer's Assistance (Carer Support Payment) (Scotland) Regulations 2023 reg. 2", href="https://www.legislation.gov.uk/ssi/2023/302/regulation/2", ), dict(