From 7f7675a915a7dba7ab67cf5c3e86c5be5a5eda4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:20:05 +0100 Subject: [PATCH 1/2] Gate the Child Tax Credit child element on eligibility --- changelog.d/2594.fixed.md | 1 + .../parameters/gov/dwp/tax_credits/README.md | 18 + .../finance/benefit/family/tax_credits.yaml | 6 + .../ctc_child_element_eligibility.yaml | 337 ++++++++++++++++++ .../two_child_limit/ctc_age_exemption.yaml | 5 + ...st_tax_credits_automatic_award_identity.py | 96 +++++ .../variables/gov/dwp/CTC_child_element.py | 1 + 7 files changed, 464 insertions(+) create mode 100644 changelog.d/2594.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits/ctc_child_element_eligibility.yaml create mode 100644 policyengine_uk/tests/test_tax_credits_automatic_award_identity.py diff --git a/changelog.d/2594.fixed.md b/changelog.d/2594.fixed.md new file mode 100644 index 0000000000..767394b9fa --- /dev/null +++ b/changelog.d/2594.fixed.md @@ -0,0 +1 @@ +- Exclude the Child Tax Credit child element for ineligible families so phantom tax credit income does not reduce Housing Benefit or Council Tax Reduction. diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/README.md b/policyengine_uk/parameters/gov/dwp/tax_credits/README.md index 72e83ad676..12f20727e2 100644 --- a/policyengine_uk/parameters/gov/dwp/tax_credits/README.md +++ b/policyengine_uk/parameters/gov/dwp/tax_credits/README.md @@ -1 +1,19 @@ # Tax Credits + +The model includes the child, family and disability elements in the Child Tax +Credit maximum only when `is_CTC_eligible` is true. [Sections 8 and 9 of the Tax +Credits Act 2002](https://www.legislation.gov.uk/ukpga/2002/21/section/8) +set the entitlement and maximum-rate framework; [section 9](https://www.legislation.gov.uk/ukpga/2002/21/section/9) +provides the elements of that maximum. + +`would_claim_CTC` records whether the family would claim an eligible award. It +does not establish eligibility. The existing legacy-claim eligibility rule uses +reported Child Tax Credit and excludes Universal Credit claimants and families +whose legacy benefits have closed. A family that would claim but fails that +rule has no Child Tax Credit maximum to enter the tax credit income calculation. + +`tax_credits` combines the pre-minimum Child Tax Credit and Working Tax Credit +awards and applies the shared minimum-payment rule. Housing Benefit and Council +Tax Reduction read this combined award as income. Gating each element prevents +an ineligible family's child element from reducing those benefits through tax +credit income that the family does not receive. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml index 85504d160e..7844573bf8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits.yaml @@ -129,6 +129,8 @@ people: parent: age: 35 + # A reported award represents an eligible continuing CTC claim. + child_tax_credit_reported: 1000 child_1: age: 10 date_of_birth: 20140301 @@ -141,6 +143,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3] + would_claim_uc: false output: CTC_child_element: 10_365 - name: CTC child element for a third child born 6 April 2017 @@ -150,6 +153,8 @@ people: parent: age: 35 + # A reported award represents an eligible continuing CTC claim. + child_tax_credit_reported: 1000 child_1: age: 10 date_of_birth: 20140301 @@ -162,5 +167,6 @@ benunits: benunit: members: [parent, child_1, child_2, child_3] + would_claim_uc: false output: CTC_child_element: 6_910 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits/ctc_child_element_eligibility.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits/ctc_child_element_eligibility.yaml new file mode 100644 index 0000000000..ae800d43d7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/tax_credits/ctc_child_element_eligibility.yaml @@ -0,0 +1,337 @@ +# CTC maximum-rate elements require an eligible continuing claim, not merely a +# preference to claim. Tax Credits Act 2002 ss 8-9 (structural enacted text), +# HMRC TCM0114020 and Autumn Budget 2024 Annex A (2024/25 rates). +# Positive reported CTC is the existing model's continuing-claim proxy; no +# eligibility or tax-credit component is supplied directly. Earnings £4,000 are +# below the CTC-only threshold £19,995 and working hours are below WTC routes. +# Disability total £5,850 comprises £4,170 plus the separate £1,680 severe addition. +# The two downstream cases use a council tenant with no non-dependants, no +# meals/board, no assets, no other income and no Child Benefit claim. Gross +# earnings £4,000 are below even the adult allowance £90.50 * 52 = £4,706, before any +# earnings disregard or child allowance. Thus no excess income exists: HB is +# the supplied eligible rent £5,200, and Welsh CTR is the supplied net bill £1,000. +# SSCBA 1992 s 130; HB Regs 2006 regs 70-71; WSI 2013/3029 Sch 6 paras 1, 4, 6. +# This is a public household regression, not a population score or a complete +# audit of the inherited HB/CTR applicable-amount implementation. + +- name: "Case 1, claiming preference does not create Child Tax Credit eligibility." + period: 2024 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 0 + person2: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + output: + CTC_child_element: 0 + CTC_family_element: 0 + CTC_disabled_child_element: 0 + CTC_severely_disabled_child_element: 0 + is_CTC_eligible: false + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 + would_claim_CTC: true + +- name: "Case 2, an eligible continuing claim keeps the ordinary child element." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 1000 + person2: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + output: + is_CTC_eligible: true + CTC_child_element: 3455 + CTC_family_element: 0 + child_tax_credit: 3455 + working_tax_credit: 0 + tax_credits: 3455 + +- name: "Case 3, an eligible claim preserves the two-child limit." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 1000 + person2: + age: 6 + date_of_birth: 20180101 + is_claimant_or_partner: false + person3: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + person4: + age: 2 + date_of_birth: 20220101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2", "person3", "person4"] + would_claim_uc: false + output: + CTC_child_element: 6910 + CTC_family_element: 0 + child_tax_credit: 6910 + tax_credits: 6910 + +- name: "Case 4, an eligible older child keeps the family element." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 1000 + person2: + age: 10 + date_of_birth: 20140101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + output: + CTC_child_element: 3455 + CTC_family_element: 545 + child_tax_credit: 4000 + tax_credits: 4000 + +- name: "Case 5, an eligible disabled child keeps both disability additions." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 1000 + person2: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + is_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + output: + CTC_child_element: 3455 + CTC_disabled_child_element: 4170 + CTC_severely_disabled_child_element: 1680 + child_tax_credit: 9305 + tax_credits: 9305 + +- name: "Case 6, an ineligible family receives no ordinary or disability elements." + period: 2024 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 0 + person2: + age: 10 + date_of_birth: 20140101 + is_claimant_or_partner: false + person3: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + is_disabled_for_benefits: true + is_severely_disabled_for_benefits: true + benunits: + benunit: + members: ["person1", "person2", "person3"] + would_claim_uc: false + output: + CTC_child_element: 0 + CTC_family_element: 0 + CTC_disabled_child_element: 0 + CTC_severely_disabled_child_element: 0 + is_CTC_eligible: false + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 + +- name: "Case 7, closed tax credits pay no award in annual 2026." + period: 2026 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 1000 + person2: + age: 6 + date_of_birth: 20200101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + output: + is_CTC_eligible: false + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 + +- name: "Case 8, a childless family has no child elements." + period: 2024 + input: + people: + person1: + age: 35 + is_parent: false + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 0 + benunits: + benunit: + members: ["person1"] + would_claim_uc: false + output: + CTC_child_element: 0 + CTC_family_element: 0 + CTC_disabled_child_element: 0 + CTC_severely_disabled_child_element: 0 + is_CTC_eligible: false + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 + +- name: "Case 9, an ineligible CTC preference does not reduce Housing Benefit or Welsh Council Tax Reduction." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 0 + housing_benefit_reported: 1000 + person2: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + would_claim_CTC: true + would_claim_housing_benefit: true + would_claim_council_tax_reduction: true + would_claim_child_benefit: false + households: + household: + members: ["person1", "person2"] + region: "WALES" + tenure_type: "RENT_FROM_COUNCIL" + rent: 5200 + council_tax: 1000 + savings: 0 + output: + housing_benefit: 5200 + council_tax_reduction: 1000 + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 + +- name: "Case 10, declining CTC preserves the same full housing and council tax awards." + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 35 + is_parent: true + is_benunit_head: true + is_claimant_or_partner: true + employment_income: 4000 + weekly_hours: 10 + child_tax_credit_reported: 0 + housing_benefit_reported: 1000 + person2: + age: 4 + date_of_birth: 20200101 + is_claimant_or_partner: false + benunits: + benunit: + members: ["person1", "person2"] + would_claim_uc: false + would_claim_CTC: false + would_claim_housing_benefit: true + would_claim_council_tax_reduction: true + would_claim_child_benefit: false + households: + household: + members: ["person1", "person2"] + region: "WALES" + tenure_type: "RENT_FROM_COUNCIL" + rent: 5200 + council_tax: 1000 + savings: 0 + output: + housing_benefit: 5200 + council_tax_reduction: 1000 + child_tax_credit: 0 + working_tax_credit: 0 + tax_credits: 0 diff --git a/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml b/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml index 44d4df7a05..1d1076ca10 100644 --- a/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml +++ b/policyengine_uk/tests/policy/reforms/parametric/two_child_limit/ctc_age_exemption.yaml @@ -17,6 +17,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] + would_claim_uc: false claims_all_entitled_benefits: true output: CTC_child_element: 6470 # CTC for only the first 2 children @@ -41,6 +42,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] + would_claim_uc: false claims_all_entitled_benefits: true output: CTC_child_element: 12940 # CTC for all children @@ -65,6 +67,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] + would_claim_uc: false claims_all_entitled_benefits: true output: CTC_child_element: 6470 # CTC for only first 2 children @@ -88,6 +91,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] + would_claim_uc: false claims_all_entitled_benefits: true output: CTC_child_element: 6470 # CTC for only first 2 children @@ -112,6 +116,7 @@ benunits: benunit: members: [parent, child_1, child_2, child_3, child_4] + would_claim_uc: false claims_all_entitled_benefits: true output: CTC_child_element: 12940 # CTC for all children due to exemption \ No newline at end of file diff --git a/policyengine_uk/tests/test_tax_credits_automatic_award_identity.py b/policyengine_uk/tests/test_tax_credits_automatic_award_identity.py new file mode 100644 index 0000000000..cb0c7b9ee0 --- /dev/null +++ b/policyengine_uk/tests/test_tax_credits_automatic_award_identity.py @@ -0,0 +1,96 @@ +"""Automatic tax credits equal the automatic CTC and WTC awards. + +HMRC's finalised-award definitions describe tax credits as Child Tax Credit +and Working Tax Credit. Generated public situations vary family composition, +earnings, hours and reported continuing claims. No tax-credit component, +eligibility, award, rate or aggregate is supplied directly. +""" + +import numpy as np +from hypothesis import Phase, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + + +YEAR = 2024 +FAMILIES = st.lists( + st.tuples( + st.integers(min_value=1, max_value=2), + st.integers(min_value=0, max_value=3), + st.sampled_from([0, 4_000, 8_000, 25_000]), + st.sampled_from([0, 10, 16, 24, 30]), + st.sampled_from([0, 1_000]), + st.sampled_from([0, 1_000]), + st.booleans(), + ), + min_size=1, + max_size=3, +) + + +@settings( + max_examples=12, + deadline=None, + derandomize=True, + database=None, + phases=[Phase.generate], +) +@given(FAMILIES) +def test_tax_credits_equal_automatic_child_and_working_tax_credit(families): + # A non-reporting family keeps the original eligibility defect represented + # in every population, alongside the generated continuing-claim controls. + families = [(1, 1, 4_000, 10, 0, 0, False), *families] + people = {} + benunits = {} + households = {} + for index, (adults, children, earnings, hours, ctc, wtc, reverse) in enumerate( + families + ): + members = [] + for adult in range(adults): + name = f"adult_{index}_{adult}" + members.append(name) + people[name] = { + "age": {YEAR: 35 + adult}, + "is_parent": {YEAR: children > 0}, + "is_benunit_head": {YEAR: adult == 0}, + "is_claimant_or_partner": {YEAR: True}, + "employment_income": {YEAR: earnings if adult == 0 else 0}, + "weekly_hours": {YEAR: hours if adult == 0 else 0}, + "child_tax_credit_reported": {YEAR: ctc if adult == 0 else 0}, + "working_tax_credit_reported": {YEAR: wtc if adult == 0 else 0}, + } + for child in range(children): + name = f"child_{index}_{child}" + members.append(name) + age = [2, 4, 10][child] + people[name] = { + "age": {YEAR: age}, + "date_of_birth": {YEAR: (YEAR - age) * 10_000 + 101}, + "is_claimant_or_partner": {YEAR: False}, + } + if reverse: + members.reverse() + benunits[f"benunit_{index}"] = { + "members": members, + "would_claim_uc": {YEAR: False}, + } + households[f"household_{index}"] = { + "members": members, + "region": {YEAR: "WALES"}, + } + simulation = Simulation( + situation={ + "people": people, + "benunits": benunits, + "households": households, + } + ) + np.testing.assert_allclose( + simulation.calculate("tax_credits", YEAR), + simulation.calculate("child_tax_credit", YEAR) + + simulation.calculate("working_tax_credit", YEAR), + rtol=0, + atol=0.01, + ) diff --git a/policyengine_uk/variables/gov/dwp/CTC_child_element.py b/policyengine_uk/variables/gov/dwp/CTC_child_element.py index e4f309150b..3d842b32cd 100644 --- a/policyengine_uk/variables/gov/dwp/CTC_child_element.py +++ b/policyengine_uk/variables/gov/dwp/CTC_child_element.py @@ -8,6 +8,7 @@ class CTC_child_element(Variable): definition_period = YEAR reference = "Tax Credits Act 2002 s. 9" unit = GBP + defined_for = "is_CTC_eligible" def formula(benunit, period, parameters): person = benunit.members From b454cea7f76f1fb57e7a9a0706765ea9f5bb2e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:19:35 +0100 Subject: [PATCH 2/2] Add continuing CTC premises to claimant fixtures --- .../claimant_not_own_child_or_qualifying_young_person.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/claimant_not_own_child_or_qualifying_young_person.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/claimant_not_own_child_or_qualifying_young_person.yaml index ee8d69ebf7..bc36de4d7a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/claimant_not_own_child_or_qualifying_young_person.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/claimant_not_own_child_or_qualifying_young_person.yaml @@ -125,6 +125,8 @@ age: 40 is_parent: true is_claimant_or_partner: true + # Continuing CTC claim; this fixture does not claim Universal Credit. + child_tax_credit_reported: 1_000 young_person: age: 17 current_education: UPPER_SECONDARY @@ -132,6 +134,7 @@ benunits: benunit: members: [parent, young_person] + would_claim_uc: false output: is_child_or_qualifying_young_person_for_child_tax_credit: [false, true] is_responsible_for_child_or_qualifying_young_person_for_child_tax_credit: true @@ -147,12 +150,15 @@ current_education: UPPER_SECONDARY is_parent: true is_claimant_or_partner: true + # Continuing CTC claim; this fixture does not claim Universal Credit. + child_tax_credit_reported: 1_000 baby: age: 1 is_claimant_or_partner: false benunits: benunit: members: [parent, baby] + would_claim_uc: false output: is_child_or_qualifying_young_person_for_child_tax_credit: [false, true] CTC_child_element: 3_455