diff --git a/changelog.d/uc-childcare-partner-exceptions.added.md b/changelog.d/uc-childcare-partner-exceptions.added.md new file mode 100644 index 0000000000..2e833de946 --- /dev/null +++ b/changelog.d/uc-childcare-partner-exceptions.added.md @@ -0,0 +1 @@ +- Add `uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period` (reg 32(1)(a)) and `uc_is_temporarily_absent_from_claimant_household` (reg 32(1)(b)(iii)) inputs, and `uc_childcare_treated_as_in_paid_work` and `uc_unable_to_provide_childcare` for the Universal Credit childcare work condition. diff --git a/changelog.d/uc-childcare-partner-exceptions.fixed.md b/changelog.d/uc-childcare-partner-exceptions.fixed.md new file mode 100644 index 0000000000..60382d9107 --- /dev/null +++ b/changelog.d/uc-childcare-partner-exceptions.fixed.md @@ -0,0 +1 @@ +- Apply the Universal Credit childcare work condition's partner exceptions and treated-as-working rules (UC Regs 2013 reg 32). A couple now meets it when one member is in paid work and the other is in paid work or unable to provide childcare because of limited capability for work, caring for a severely disabled person or temporary absence. A claimant receiving statutory sick, maternity or paternity pay or maternity allowance is treated as in paid work. 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 55a589cd02..fb874c9956 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 @@ -200,7 +200,8 @@ 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. + # The partner neither works nor meets an exception in (b)(i)-(iii): + # 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/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 new file mode 100644 index 0000000000..0c43d5ad79 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml @@ -0,0 +1,564 @@ +# Universal Credit Regulations 2013 reg. 32, the childcare work condition: +# https://www.legislation.gov.uk/uksi/2013/376/regulation/32 +# "(1) The work condition is met in respect of an assessment period if— +# (a) the claimant is in paid work or has an offer of paid work that is due +# to start before the end of the next assessment period; and +# (b) if the claimant is a member of a couple (whether claiming jointly or as +# a single person), the other member is either in paid work or is unable +# to provide childcare because that person— (i) has limited capability +# for work, (ii) has regular and substantial caring responsibilities for +# a severely disabled person, or (iii) is temporarily absent from the +# claimant's household." +# Reg. 32(2)(b): a claimant receiving statutory sick pay, statutory maternity +# pay, statutory paternity pay, ... or a maternity allowance is treated as in +# paid work. ADM ch. F7, F7011-F7015, agrees: +# https://assets.publishing.service.gov.uk/media/696a076c7b7f37aa8e4022d9/adm-ch-f7.pdf +# +# Where an element amount is checked, it is reg. 34 for one child in 2025-26: +# min(5,000 * 85%, 1,031.88 * 12) = 4,250. + +- name: Reg. 32(1)(b)(i) - a working claimant's partner with limited capability for work + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + is_parent: true + employment_income: 15_000 + partner: + age: 34 + uc_limited_capability_for_WRA: true + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + # (a) the claimant works; (b) the partner does not, but has limited + # capability for work, so is unable to provide childcare. + uc_unable_to_provide_childcare: [false, true, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Reg. 32(1)(b)(i) - the limited capability proxy follows is_disabled_for_benefits + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + employment_income: 15_000 + partner: + age: 34 + is_disabled_for_benefits: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # uc_limited_capability_for_WRA defaults to is_disabled_for_benefits. + uc_limited_capability_for_WRA: [false, true] + uc_childcare_work_condition: true + +- name: Reg. 32(1)(b)(ii) - ADM F7013 example, a partner caring full time for her disabled mother + period: 2025 + absolute_error_margin: 0.01 + input: + people: + ben: + age: 40 + is_parent: true + employment_income: 9_000 + sara: + age: 38 + care_hours: 40 + pauline: + age: 4 + childcare_expenses: 5_000 + mother: + age: 70 + aa_category: HIGHER + benunits: + benunit: + members: [ben, sara, pauline] + mothers_benunit: + members: [mother] + households: + household: + members: [ben, sara, pauline, mother] + output: + # Ben works two days a week. Sara provides full-time care for her mother, + # who lives with them and receives the higher rate of attendance + # 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 + # does not check her mother's attendance allowance. + is_carer_for_benefits: [false, true, false, false] + uc_childcare_work_condition: [true, false] + uc_childcare_element: [4_250, 0] + +- name: Reg. 32(1)(b)(ii) - a partner receiving Carer's Allowance + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + employment_income: 15_000 + partner: + age: 34 + receives_carers_allowance: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # Reg. 30(1)(a): satisfies the conditions for Carer's Allowance. + uc_unable_to_provide_childcare: [false, true] + uc_childcare_work_condition: true + +- name: Reg. 32(1)(b)(iii) - a partner temporarily absent from the claimant's household + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + is_parent: true + employment_income: 15_000 + partner: + age: 34 + uc_is_temporarily_absent_from_claimant_household: true + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + uc_unable_to_provide_childcare: [false, true, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Reg. 32(1)(b) - either joint claimant can be the one in paid work + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 35 + uc_limited_capability_for_WRA: true + partner: + age: 34 + employment_income: 15_000 + benunits: + benunit: + members: [head, partner] + households: + household: + members: [head, partner] + output: + # Each joint claimant is "the claimant" (WRA 2012 s. 40): the working + # partner meets (a), the head (limited capability for work) meets (b). + uc_childcare_work_condition: true + +- name: Reg. 32(1)(b) - a working claimant's nonworking partner without an exception + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + is_parent: true + employment_income: 15_000 + partner: + age: 34 + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + uc_unable_to_provide_childcare: [false, false, false] + uc_childcare_work_condition: false + uc_childcare_element: 0 + +- name: Reg. 32(1)(a) - the exceptions do not replace the claimant's own paid work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_limited_capability_for_WRA: true + partner: + age: 34 + care_hours: 40 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # Neither member is in paid work, so neither meets (a): ADM F7013 applies + # "where only one of the couple is in paid work". + uc_unable_to_provide_childcare: [true, true] + uc_childcare_work_condition: false + +- name: Reg. 32(1)(a) - a nonworking single claimant with limited capability for work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_limited_capability_for_WRA: true + output: + # Limb (b) and its exceptions apply only to the other member of a couple. + uc_childcare_work_condition: false + +- name: Reg. 32(2)(b) - a single claimant receiving statutory maternity pay + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_parent: true + statutory_maternity_pay: 6_000 + child: + age: 0 + childcare_expenses: 5_000 + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + in_work: [false, false] + uc_childcare_treated_as_in_paid_work: [true, false] + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Reg. 32(2)(b) - a single claimant receiving statutory sick pay + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + statutory_sick_pay: 1_500 + output: + uc_childcare_treated_as_in_paid_work: true + uc_childcare_work_condition: true + +- name: Reg. 32(2)(b) - a single claimant receiving statutory paternity pay + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + statutory_paternity_pay: 400 + output: + uc_childcare_treated_as_in_paid_work: true + uc_childcare_work_condition: true + +- name: Reg. 32(2)(b) - a single claimant receiving maternity allowance + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + maternity_allowance_reported: 5_000 + output: + uc_childcare_treated_as_in_paid_work: true + uc_childcare_work_condition: true + +- name: Reg. 32(2)(b) - a claimant on statutory sick pay with a partner with limited capability for work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + statutory_sick_pay: 1_500 + partner: + age: 34 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # The claimant is treated as in paid work (32(2)(b)) and the partner is + # unable to provide childcare (32(1)(b)(i)). + uc_childcare_work_condition: true + +- name: Reg. 32(2)(b) - joint claimants both treated as in paid work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + statutory_sick_pay: 1_500 + partner: + age: 34 + statutory_maternity_pay: 6_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # Reg. 32(2) treats "a claimant" as in paid work, and each joint claimant + # is a claimant (WRA 2012 s. 40), so each is treated as in paid work. + uc_childcare_treated_as_in_paid_work: [true, true] + uc_childcare_work_condition: true + +- name: Reg. 32(2)(b) - a partner on statutory maternity pay with a working claimant + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + is_parent: true + employment_income: 15_000 + partner: + age: 34 + statutory_maternity_pay: 6_000 + child: + age: 0 + childcare_expenses: 5_000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + uc_childcare_work_condition: true + uc_childcare_element: 4_250 + +- name: Reg. 32(1)(a)-(b) - a claimant with an offer of paid work and a partner on maternity allowance + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + partner: + age: 34 + maternity_allowance_reported: 5_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # The claimant meets only (a), with the offer. The partner, a joint + # claimant receiving maternity allowance, is treated as in paid work + # (32(2)(b), WRA 2012 s. 40), so meets (b). Taking the partner as the + # claimant instead fails, because an offer does not meet (b). + uc_childcare_treated_as_in_paid_work: [false, true] + uc_childcare_work_condition: true + +- name: Reg. 32(1)(a) - a single claimant with an offer of paid work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + output: + # ADM F7011 example 1: Colin has secured a job due to start before the + # end of the next assessment period. + uc_childcare_work_condition: true + +- name: Reg. 32(1)(a) - a claimant with an offer of paid work and a working partner + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + partner: + age: 34 + employment_income: 15_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # The claimant meets (a) with the offer; the partner meets (b) by working. + uc_childcare_work_condition: true + +- name: Reg. 32(1)(a) - a claimant with an offer of paid work and a partner with limited capability for work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + partner: + age: 34 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + uc_childcare_work_condition: true + +- name: Reg. 32(1)(a) - a claimant with an offer of paid work and a nonworking partner + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + partner: + age: 34 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + uc_childcare_work_condition: false + +- name: Reg. 32(1)(b) - an offer of paid work does not meet the other member's limb + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + partner: + age: 34 + uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # Whichever member is taken as the claimant meets (a) with an offer, but + # (b) requires the other member to be "in paid work" or unable to provide + # childcare; an offer is neither. + uc_childcare_work_condition: false + +- name: Reg. 32(1) - three flagged claimants, one in paid work and two unable to provide childcare + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + employment_income: 15_000 + second: + age: 39 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + third: + age: 38 + is_claimant_or_partner: true + care_hours: 40 + benunits: + benunit: + members: [first, second, third] + households: + household: + members: [first, second, third] + output: + # A couple has two members, so three flagged claimants are a data error, + # not a legal claim. The formula generalises reg. 32(1) as: some claimant + # meets limb (a) and every other claimant meets limb (b). + uc_childcare_work_condition: true + +- name: Reg. 32(1) - three flagged claimants, one of whom neither works nor meets an exception + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + employment_income: 15_000 + second: + age: 39 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + third: + age: 38 + is_claimant_or_partner: true + benunits: + benunit: + members: [first, second, third] + households: + household: + members: [first, second, third] + output: + uc_childcare_work_condition: false + +- name: Reg. 32(1)(a) - self-employment is paid work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 35 + self_employment_income: 8_000 + partner: + age: 34 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + # Reg. 2: paid work is "work done for payment or in expectation of + # payment"; the in_work proxy counts self-employment earnings. + in_work: [true, false] + uc_childcare_work_condition: true diff --git a/policyengine_uk/tests/test_uc_childcare_work_condition_properties.py b/policyengine_uk/tests/test_uc_childcare_work_condition_properties.py new file mode 100644 index 0000000000..7b9547ff31 --- /dev/null +++ b/policyengine_uk/tests/test_uc_childcare_work_condition_properties.py @@ -0,0 +1,370 @@ +"""Property-based tests for the Universal Credit childcare work condition. + +Universal Credit Regulations 2013 reg. 32(1): the condition is met if (a) the +claimant is in paid work or has an offer of paid work due to start before the +end of the next assessment period, and (b) for a couple, the other member is +in paid work or is unable to provide childcare because they (i) have limited +capability for work, (ii) have regular and substantial caring +responsibilities for a severely disabled person, or (iii) are temporarily +absent from the claimant's household. Reg. 32(2)(b) treats a claimant +receiving statutory sick, maternity or paternity pay or maternity allowance +as in paid work. Each joint claimant is a claimant (WRA 2012 s. 40). + +Invariants, for any generated population of single claimants and couples, +each with a dependant child or young person. Workers are generated with +employment income, self-employment income or hours alone, the three routes +into the in_work proxy. The reference applies the model's approximations for +the exceptions, not the full legal tests: limited capability for work is the +uc_limited_capability_for_WRA input, and caring is at least the Carer's +Allowance minimum hours of care (the hours branch of is_carer_for_benefits). + +1. Differential: the condition equals a reference transcribed from reg. 32 in + its pairwise form, (a)(A) and (b)(B), or (a)(B) and (b)(A). +2. Monotonic: giving any member an exception, a listed payment, paid work or + an offer of paid work never removes the condition. +3. A couple who are both in paid work always meet the condition. +4. Without a member meeting limb (a), no exception meets the condition. +5. Swapping the two members of a couple does not change the result. +6. A dependant's work, payments and circumstances never change the result. +7. The childcare element is zero whenever the condition fails, and never + negative. + +Every member's is_claimant_or_partner is set from its generated role, as the +FRS supplies it, and every attribute is set for every member: setting an +input for anyone makes it an input for everyone. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Reg. 32(2)(b) payments the model has variables for. +PAYMENTS = [ + None, + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + "maternity_allowance_reported", +] +CARE_HOURS = [0, 20, 34, 35, 50] +# The routes into the in_work proxy: positive earnings of either kind, or +# positive hours. +WORK_SOURCES = ["employment", "self_employment", "hours"] +# Attributes that can only add a route to the condition. +WIDENING = ["works", "payment", "offer", "lcw", "carer", "absent"] + + +@st.composite +def members(draw): + # Each member has a small set of routes, so that members with exactly one + # route (an offer only, an exception only) are common. + routes = draw(st.sets(st.sampled_from(WIDENING), max_size=3)) + payments = PAYMENTS[1:] if "payment" in routes else [None] + carer_hours = [ + h for h in CARE_HOURS if (h >= MIN_CARE_HOURS) == ("carer" in routes) + ] + return dict( + works=draw(st.sampled_from(WORK_SOURCES)) if "works" in routes else None, + payment=draw(st.sampled_from(payments)), + offer="offer" in routes, + lcw="lcw" in routes, + care_hours=draw(st.sampled_from(carer_hours)), + absent="absent" in routes, + ) + + +@st.composite +def families(draw): + adults = draw(st.lists(members(), min_size=1, max_size=2)) + return dict( + adults=adults, + dependant=draw(members()), + dependant_age=draw(st.sampled_from([3, 17, 18])), + childcare=draw(st.sampled_from([0.0, 2_000.0, 20_000.0])), + ) + + +def person_inputs(member, age, claimant_or_partner): + inputs = { + "age": age, + "is_claimant_or_partner": claimant_or_partner, + "employment_income": 12_000.0 if member["works"] == "employment" else 0.0, + "self_employment_income": ( + 12_000.0 if member["works"] == "self_employment" else 0.0 + ), + "hours_worked": 1_000.0 if member["works"] == "hours" else 0.0, + "uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period": member[ + "offer" + ], + "uc_limited_capability_for_WRA": member["lcw"], + "care_hours": float(member["care_hours"]), + "uc_is_temporarily_absent_from_claimant_household": member["absent"], + } + for payment in PAYMENTS[1:]: + inputs[payment] = 1_000.0 if member["payment"] == payment else 0.0 + return {name: {YEAR: value} for name, value in inputs.items()} + + +def situation(units, with_dependants=True): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"p{i}_{j}" + people[name] = person_inputs(adult, 30 + j, True) + names.append(name) + if with_dependants: + name = f"p{i}_dependant" + people[name] = person_inputs( + unit["dependant"], unit["dependant_age"], False + ) + people[name]["childcare_expenses"] = {YEAR: unit["childcare"]} + if unit["dependant_age"] >= 16: + people[name]["current_education"] = {YEAR: "UPPER_SECONDARY"} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = {"members": names} + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, with_dependants=True): + sim = Simulation(situation=situation(units, with_dependants)) + return { + variable: np.asarray(sim.calculate(variable, YEAR)) + for variable in ["uc_childcare_work_condition", "uc_childcare_element"] + } + + +def min_care_hours(): + sim = Simulation(situation={"people": {"p": {"age": {YEAR: 30}}}}) + return sim.tax_benefit_system.parameters(YEAR).gov.dwp.carers_allowance.min_hours + + +MIN_CARE_HOURS = min_care_hours() + + +def meets_claimant_limb(member): + # Reg. 32(1)(a) with reg. 32(2)(b). + return bool(member["works"]) or member["payment"] is not None or member["offer"] + + +def meets_other_member_limb(member): + # Reg. 32(1)(b); joint claimants also have reg. 32(2)(b). + unable = member["lcw"] or member["care_hours"] >= MIN_CARE_HOURS or member["absent"] + return bool(member["works"]) or member["payment"] is not None or bool(unable) + + +def reference(unit): + adults = unit["adults"] + if len(adults) == 1: + return meets_claimant_limb(adults[0]) + first, second = adults + return (meets_claimant_limb(first) and meets_other_member_limb(second)) or ( + meets_claimant_limb(second) and meets_other_member_limb(first) + ) + + +def with_member(unit, index, **changes): + adults = [dict(adult) for adult in unit["adults"]] + adults[index].update(changes) + return dict(unit, adults=adults) + + +WIDEN = { + "works": dict(works="hours"), + "payment": dict(payment="statutory_sick_pay"), + "offer": dict(offer=True), + "lcw": dict(lcw=True), + "carer": dict(care_hours=50), + "absent": dict(absent=True), +} + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_condition_matches_the_regulation_32_reference(units): + values = calculate(units) + for i, unit in enumerate(units): + assert bool(values["uc_childcare_work_condition"][i]) == reference(unit), unit + + +def route_sets(): + return [ + {route for k, route in enumerate(WIDENING) if mask >> k & 1} + for mask in range(2 ** len(WIDENING)) + ] + + +def member_with(routes, payment, care_hours, work_source): + return dict( + works=work_source if "works" in routes else None, + payment=payment if "payment" in routes else None, + offer="offer" in routes, + lcw="lcw" in routes, + # A non-carer cares for just under the minimum, so the threshold, not + # the absence of any care, separates the two. + care_hours=care_hours if "carer" in routes else MIN_CARE_HOURS - 1, + absent="absent" in routes, + ) + + +def test_condition_matches_the_reference_for_every_combination_of_routes(): + # Exhaustive over route sets: every single claimant's set of routes with + # every listed payment and every kind of work, and every pair of route + # sets for a couple, the payment and the kind of work cycling across + # pairs. A carer in a couple cares for exactly the minimum hours if the + # first member and 50 if the second; a non-carer for one hour under the + # minimum. Where one member has only an offer of work and the other only + # a payment, every payment is tried, since that is where a payment alone + # decides limb (b). Every family has a dependant: idle in half of them, + # otherwise with a cycling set of routes of its own. + sets = route_sets() + + def dependant(k): + routes = set() if k % 2 == 0 else sets[(5 * k + 3) % len(sets)] + return dict( + dependant=member_with(routes, PAYMENTS[1 + k % 4], 50, WORK_SOURCES[k % 3]), + dependant_age=[3, 17, 18][k % 3], + childcare=[0.0, 5_000.0][k % 2], + ) + + singles = [ + dict(adults=[member_with(routes, payment, MIN_CARE_HOURS, source)]) + for routes in sets + for payment in PAYMENTS[1:] + for source in WORK_SOURCES + ] + couples = [ + dict( + adults=[ + member_with( + first, + PAYMENTS[1 + (i + j) % 4], + MIN_CARE_HOURS, + WORK_SOURCES[(i + j) % 3], + ), + member_with( + second, + PAYMENTS[1 + (i + 2 * j) % 4], + 50, + WORK_SOURCES[(i + 2 * j + 1) % 3], + ), + ] + ) + for i, first in enumerate(sets) + for j, second in enumerate(sets) + ] + offer_only, payment_only = {"offer"}, {"payment"} + offer_with_payment = [ + dict( + adults=[ + member_with(offer_only, None, MIN_CARE_HOURS, "employment"), + member_with(payment_only, payment, 50, "employment"), + ][::order] + ) + for payment in PAYMENTS[1:] + for order in (1, -1) + ] + units = [ + dict(unit, **dependant(k)) + for k, unit in enumerate(singles + couples + offer_with_payment) + ] + met = calculate(units)["uc_childcare_work_condition"] + expected = np.array([reference(unit) for unit in units]) + mismatches = [unit for unit, m, e in zip(units, met, expected) if m != e] + assert not mismatches, mismatches[:5] + + +@PROPERTY_SETTINGS +@given( + st.lists( + st.tuples(families(), st.integers(0, 1), st.sampled_from(WIDENING)), + min_size=1, + max_size=30, + ) +) +def test_adding_an_exception_or_work_never_removes_the_condition(cases): + units = [unit for unit, _, _ in cases] + widened = [ + with_member(unit, index % len(unit["adults"]), **WIDEN[route]) + for unit, index, route in cases + ] + before = calculate(units)["uc_childcare_work_condition"] + after = calculate(widened)["uc_childcare_work_condition"] + assert not np.any(before & ~after), [ + case for case, b, a in zip(cases, before, after) if b and not a + ] + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_a_couple_both_in_paid_work_always_meets_the_condition(units): + couples = [ + dict( + unit, + adults=[ + dict(adult, works=adult["works"] or "self_employment") + for adult in unit["adults"] + ], + ) + for unit in units + if len(unit["adults"]) == 2 + ] + if couples: + assert calculate(couples)["uc_childcare_work_condition"].all() + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_no_exception_meets_the_condition_without_limb_a(units): + idle = [ + dict( + unit, + adults=[ + dict(adult, works=None, payment=None, offer=False) + for adult in unit["adults"] + ], + ) + for unit in units + ] + assert not calculate(idle)["uc_childcare_work_condition"].any() + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_swapping_the_members_of_a_couple_does_not_change_the_result(units): + couples = [unit for unit in units if len(unit["adults"]) == 2] + if not couples: + return + swapped = [dict(unit, adults=unit["adults"][::-1]) for unit in couples] + np.testing.assert_array_equal( + calculate(couples)["uc_childcare_work_condition"], + calculate(swapped)["uc_childcare_work_condition"], + ) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_dependants_do_not_affect_the_condition(units): + np.testing.assert_array_equal( + calculate(units)["uc_childcare_work_condition"], + calculate(units, with_dependants=False)["uc_childcare_work_condition"], + ) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=30)) +def test_childcare_element_needs_the_work_condition(units): + values = calculate(units) + element = values["uc_childcare_element"] + assert np.all(element >= 0) + assert np.all(element[~values["uc_childcare_work_condition"]] == 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_treated_as_in_paid_work.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_treated_as_in_paid_work.py new file mode 100644 index 0000000000..d861e20d7f --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_treated_as_in_paid_work.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * + + +class uc_childcare_treated_as_in_paid_work(Variable): + value_type = bool + entity = Person + label = "Treated as in paid work for the Universal Credit childcare work condition" + documentation = ( + "Regulation 32(2)(b): a claimant receiving statutory sick pay, " + "statutory maternity pay, statutory paternity pay or maternity " + "allowance is treated as in paid work for the work condition. The " + "regulation also lists statutory adoption pay, statutory shared " + "parental pay, statutory parental bereavement pay and statutory " + "neonatal care pay, which the model has no variables for. Regulation " + "32(2)(a), ceasing paid work in the current or previous assessment " + "period, is covered only as the annual in_work proxy covers it: hours " + "or earnings at any point in the year count as paid work." + ) + definition_period = YEAR + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 32(2)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/32", + ), + dict( + title="Advice for Decision Making ch. F7, para. F7015", + href="https://assets.publishing.service.gov.uk/media/696a076c7b7f37aa8e4022d9/adm-ch-f7.pdf", + ), + ] + + def formula(person, period, parameters): + statutory_payments = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + "maternity_allowance", + ] + receives = [person(payment, period) > 0 for payment in statutory_payments] + return np.logical_or.reduce(receives) 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 661786c65f..86ef872a7e 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 @@ -6,16 +6,56 @@ class uc_childcare_work_condition(Variable): 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." + "Regulation 32(1): the claimant is in paid work or has an offer of " + "paid work due to start before the end of the next assessment period, " + "and, for a couple, the other member is either in paid work or unable " + "to provide childcare because they have limited capability for work, " + "have regular and substantial caring responsibilities for a severely " + "disabled person, or are temporarily absent from the claimant's " + "household. Each joint claimant is a claimant, so the condition is met " + "when one member meets limb (a) and the other meets limb (b). Paid " + "work is the in_work proxy (positive hours or earnings in the year), " + "or being treated as in paid work under regulation 32(2) " + "(uc_childcare_treated_as_in_paid_work). The exceptions use the " + "approximations in uc_unable_to_provide_childcare. Dependants are not " + "tested." ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32" + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 32", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/32", + ), + dict( + title="Advice for Decision Making ch. F7, paras. F7011-F7015", + href="https://assets.publishing.service.gov.uk/media/696a076c7b7f37aa8e4022d9/adm-ch-f7.pdf", + ), + ] def formula(benunit, period, parameters): person = benunit.members claimant = person("is_uc_claimant", period) - in_work = person("in_work", period) - return benunit.any(claimant & in_work) & benunit.all(in_work | ~claimant) + in_paid_work = person("in_work", period) | person( + "uc_childcare_treated_as_in_paid_work", period + ) + # Reg. 32(1)(a): in paid work or has an offer of paid work due to + # start before the end of the next assessment period. + meets_claimant_limb = in_paid_work | person( + "uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period", + period, + ) + # Reg. 32(1)(b): the other member is in paid work or is unable to + # provide childcare for a reason in (i)-(iii). An offer of paid work + # satisfies limb (a) only. + meets_other_member_limb = in_paid_work | person( + "uc_unable_to_provide_childcare", period + ) + # Joint claimants are each "the claimant" (WRA 2012 s. 40), so the + # condition holds if any claimant meets limb (a) while every other + # claimant meets limb (b). A single claimant has no other member. + fails_other_member_limb = claimant & ~meets_other_member_limb + others_failing = ( + benunit.project(benunit.sum(fails_other_member_limb)) + - fails_other_member_limb + ) + return benunit.any(claimant & meets_claimant_limb & (others_failing == 0)) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period.py new file mode 100644 index 0000000000..8fefd90de2 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class uc_has_offer_of_paid_work_starting_by_end_of_next_assessment_period(Variable): + value_type = bool + entity = Person + label = "Has an offer of paid work due to start before the end of the next Universal Credit assessment period" + documentation = ( + "Regulation 32(1)(a): a claimant with an offer of paid work due to " + "start before the end of the next assessment period meets the " + "claimant's limb of the childcare work condition. It does not meet " + "the other member's limb in regulation 32(1)(b). Survey data does not " + "record offers of work, so this defaults to false." + ) + definition_period = YEAR + default_value = False + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 32(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/32", + ), + dict( + title="Advice for Decision Making ch. F7, paras. F7011-F7012", + href="https://assets.publishing.service.gov.uk/media/696a076c7b7f37aa8e4022d9/adm-ch-f7.pdf", + ), + ] 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 new file mode 100644 index 0000000000..72de5b28f8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_unable_to_provide_childcare.py @@ -0,0 +1,53 @@ +from policyengine_uk.model_api import * + + +class uc_unable_to_provide_childcare(Variable): + value_type = bool + entity = Person + label = "Unable to provide childcare for the Universal Credit work condition" + documentation = ( + "Regulation 32(1)(b): the other member of a couple who is not in paid " + "work still lets the claimant meet the work condition if they are " + "unable to provide childcare because they (i) have limited capability " + "for work, (ii) have regular and substantial caring responsibilities " + "for a severely disabled person, or (iii) are temporarily absent from " + "the claimant's household. Each limb is an approximation. Limb (i) " + "reads uc_limited_capability_for_WRA, which defaults to " + "is_disabled_for_benefits (disability benefit receipt in survey " + "data), not an observed work capability assessment; limited " + "capability for work and work-related activity includes limited " + "capability for work. 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 " + "uc_is_temporarily_absent_from_claimant_household." + ) + definition_period = YEAR + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 32(1)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/32", + ), + dict( + title="Universal Credit Regulations 2013 reg. 30", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/30", + ), + dict( + title="Social Security Contributions and Benefits Act 1992 s. 70", + href="https://www.legislation.gov.uk/ukpga/1992/4/section/70", + ), + dict( + title="Advice for Decision Making ch. F7, para. F7013", + href="https://assets.publishing.service.gov.uk/media/696a076c7b7f37aa8e4022d9/adm-ch-f7.pdf", + ), + ] + + def formula(person, period, parameters): + return ( + person("uc_limited_capability_for_WRA", period) + | person("is_carer_for_benefits", period) + | person("uc_is_temporarily_absent_from_claimant_household", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_temporarily_absent_from_claimant_household.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_temporarily_absent_from_claimant_household.py new file mode 100644 index 0000000000..4fc7fa3410 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_temporarily_absent_from_claimant_household.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class uc_is_temporarily_absent_from_claimant_household(Variable): + value_type = bool + entity = Person + label = "Temporarily absent from the Universal Credit claimant's household" + documentation = ( + "Whether this member of a couple is temporarily absent from the " + "claimant's household. A couple stays a couple during a temporary " + "absence unless it exceeds, or is expected to exceed, six months " + "(regulation 3(6)). The absent member is unable to provide childcare " + "for the childcare work condition (regulation 32(1)(b)(iii)). Survey " + "data does not record it, so this defaults to false." + ) + definition_period = YEAR + default_value = False + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 32(1)(b)(iii)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/32", + ), + dict( + title="Universal Credit Regulations 2013 reg. 3(6)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ), + ]