diff --git a/changelog.d/tax-credits-pension-credit-passport.fixed.md b/changelog.d/tax-credits-pension-credit-passport.fixed.md new file mode 100644 index 0000000000..a3518c69eb --- /dev/null +++ b/changelog.d/tax-credits-pension-credit-passport.fixed.md @@ -0,0 +1,2 @@ +- Lift the tax credit income test for a claimant or couple entitled to State Pension Credit, guarantee credit or savings credit (TCA 2002 s.7(2); SI 2002/2008 reg 4(1)(d)). Working tax credit is Pension Credit income (SPCA 2002 s.15(1)(b)), so the passport applies where Pension Credit is payable with the passported award counted as income. The new parameter `gov.dwp.tax_credits.means_test.pension_credit_passport` dates the rule from 26 November 2003. +- Add `tax_credits_current_year_income`, the tax credit income before the passport (TCA 2002 s.7(4); SI 2002/2006 reg 3). `tax_credits_applicable_income` is that income, or nil while the test is lifted. The targeted childcare tax credit criteria now test the gross income against the £16,190 limit (SI 2014/2147 reg 1(2)), not the nil income of a passported award. diff --git a/policyengine_uk/parameters/gov/dwp/tax_credits/means_test/pension_credit_passport.yaml b/policyengine_uk/parameters/gov/dwp/tax_credits/means_test/pension_credit_passport.yaml new file mode 100644 index 0000000000..6c5b0ad872 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/tax_credits/means_test/pension_credit_passport.yaml @@ -0,0 +1,15 @@ +description: Whether entitlement to State Pension Credit (either credit) lifts the tax credit income test. +values: + 2003-04-06: false + # SI 2003/2815 reg 18 added reg 4(1)(d) from 26 November 2003. + 2003-11-26: true +metadata: + unit: bool + label: Pension Credit tax credit passport + reference: + - title: Tax Credits Act 2002 s. 7(2) + href: https://www.legislation.gov.uk/ukpga/2002/21/section/7 + - title: The Tax Credits (Income Thresholds and Determination of Rates) Regulations 2002 reg. 4(1)(d) + href: https://www.legislation.gov.uk/uksi/2002/2008/regulation/4 + - title: The Tax Credits (Miscellaneous Amendments No.2) Regulations 2003 reg. 18 + href: https://www.legislation.gov.uk/uksi/2003/2815/regulation/18 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.yaml index 34477085af..869923b5eb 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.yaml @@ -8,7 +8,7 @@ benunit: members: [person] child_tax_credit: 1 - tax_credits_applicable_income: 16_190 + tax_credits_current_year_income: 16_190 output: meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: true @@ -23,7 +23,7 @@ members: [person] child_tax_credit: 1 working_tax_credit: 1 - tax_credits_applicable_income: 16_190 + tax_credits_current_year_income: 16_190 output: meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false @@ -37,6 +37,6 @@ benunit: members: [person] child_tax_credit: 1 - tax_credits_applicable_income: 16_191 + tax_credits_current_year_income: 16_191 output: meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.yaml index 12ee46e9e9..7e1a35bb6c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.yaml @@ -8,7 +8,7 @@ benunit: members: [person] working_tax_credit: 1 - tax_credits_applicable_income: 16_190 + tax_credits_current_year_income: 16_190 output: meets_working_tax_credit_criteria_for_targeted_childcare_entitlement: true @@ -22,7 +22,7 @@ benunit: members: [person] child_tax_credit: 1 - tax_credits_applicable_income: 16_190 + tax_credits_current_year_income: 16_190 output: meets_working_tax_credit_criteria_for_targeted_childcare_entitlement: false @@ -36,6 +36,6 @@ benunit: members: [person] working_tax_credit: 1 - tax_credits_applicable_income: 16_191 + tax_credits_current_year_income: 16_191 output: meets_working_tax_credit_criteria_for_targeted_childcare_entitlement: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits_pension_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits_pension_credit_passport.yaml new file mode 100644 index 0000000000..14edc7aaaf --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/tax_credits_pension_credit_passport.yaml @@ -0,0 +1,454 @@ +# State Pension Credit lifts the tax credit income test. +# +# TCA 2002 s.7(2): the income test "does not apply ... for so long as the +# person, or either of the persons, is entitled to any social security benefit +# prescribed". SI 2002/2008 reg 4(1)(d) prescribes "state pension credit within +# the meaning of the State Pension Credit Act 2002", which is the guarantee +# credit, the savings credit or both (SPCA 2002 s.1(3)). +# +# Working tax credit is Pension Credit income (SPCA 2002 s.15(1)(b)). The +# passport holds when Pension Credit is payable with the passported award +# counted; when Pension Credit is payable only on the income-tested award, +# lifting the test would end it, and the income test stays. +# +# 2024-25 rates: +# - WTC basic element £2,435, couple element £2,500 (WTC Regs 2002 Sch 2). +# - Income threshold £7,955, or £19,995 for a claim with no WTC entitlement; +# withdrawal rate 41% (SI 2002/2008 regs 3 and 7, as uprated). +# - CTC child element £3,455; family element £545 (CTC Regs 2002 reg 7). +# - Standard minimum guarantee £218.15 a week single, £332.95 couple; savings +# credit threshold £301.22 couple (SPC Regs 2002 regs 6 and 7, as uprated). +# WEEKS_IN_YEAR is 52. +# +# Tax credit income (SI 2002/2006 reg 3): State Pension is step-one pension +# income (reg 5(1)(a); ITEPA 2003 s.577) and savings interest step-one +# investment income (reg 10); step one counts only its excess over £300. +# Earnings are step two. In each case the claimants have £10,000 of savings +# and at least £300 of savings interest between them, so the interest uses up +# step one's £300 and step one's excess is the State Pension plus any interest +# above £300. (The model counts State Pension in step two, which gives the +# same total once the interest uses the £300.) £10,000 of capital gives no +# Pension Credit tariff income (SPC Regs 2002 reg 15(6)), and savings interest +# is not Pension Credit income. +# +# Pension Credit earnings are entered net of the SPC Regs 2002 Sch VI para 5 +# disregard (£5 a week single, £10 a couple), which the model does not apply. +# No case has income tax: each person's income is within the personal +# allowance. + +- name: Guarantee Credit lifts the income test, with the maximum WTC counted as its income + # Tax credit income: (6,000 + 300 - 300) + 2,600 = £8,600. + # Lifting the test pays the maximum WTC, £2,435. Pension Credit income is + # then £2,340 + £6,000 + £2,435 = £10,775 against a guarantee of + # 218.15 x 52 = £11,343.80, so Guarantee Credit is £568.80 and the + # passport holds. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 6_000 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 # 2,600 - 5 x 52 + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 0 + working_tax_credit: 2_435 + guarantee_credit: 568.80 + pension_credit: 568.80 + +- name: The passport in 2018-19, before the mixed-age rule + # 2018-19 rates: WTC basic element £1,960; income threshold £6,420; + # standard minimum guarantee £163.00 a week (£8,476 a year). A man aged 66 + # reached pensionable age (65) in 2017, so has no savings credit: SPCA 2002 + # s.3(1)(a) needs pensionable age "before 6 April 2016". + # Tax credit income: (4,000 + 300 - 300) + 2,600 = £6,600. With the test + # lifted, WTC is £1,960, and Pension Credit income is 2,340 + 4,000 + 1,960 + # = £8,300, so Guarantee Credit is 8,476 - 8,300 = £176: the passport holds. + period: 2018 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 66 + gender: MALE + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 4_000 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 0 + tax_credits_current_year_income: 6_600 + working_tax_credit: 1_960 + pension_credit: 176 + +- name: Pension Credit payable only on the income-tested award leaves the income test in place + # Tax credit income: (6,600 + 300 - 300) + 2,600 = £9,200. With the test lifted, Pension + # Credit income is £2,340 + £6,600 + £2,435 = £11,375, above the £11,343.80 + # guarantee, so no Pension Credit would be payable and the test applies. + # WTC: 2,435 - 0.41 x (9,200 - 7,955) = £1,924.55. Pension Credit on that + # award: 11,343.80 - (2,340 + 6,600 + 1,924.55) = £479.25. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 6_600 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 9_200 + working_tax_credit: 1_924.55 + pension_credit: 479.25 + +- name: A pensioner with no Pension Credit keeps the income test + # Tax credit income: (9,500 + 300 - 300) + 2,600 = £12,100. Pension Credit income is at + # least £2,340 + £9,500 = £11,840, above the guarantee. + # WTC: 2,435 - 0.41 x (12,100 - 7,955) = £735.55. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 9_500 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 12_100 + working_tax_credit: 735.55 + pension_credit: 0 + +- name: Pension Credit that is not claimed does not lift the income test + # The first case's household without a Pension Credit claim: there is no + # entitlement without a claim (SSAA 1992 s.1(1)). + # WTC: 2,435 - 0.41 x (8,600 - 7,955) = £2,170.55. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 6_000 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + would_claim_pc: false + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 8_600 + working_tax_credit: 2_170.55 + pension_credit: 0 + +- name: Savings Credit alone lifts the income test for Child Tax Credit + # A couple aged 80 (State Pension age before 6 April 2016, so Savings Credit + # applies) responsible for a grandchild, claiming CTC only. + # Tax credit income: step one, State Pension £9,000 + £8,000 and savings + # interest £500, less £300, = £17,200; step two, earnings £3,000; total + # £20,200, above the £19,995 threshold. + # Pension Credit income: £17,000 + (3,000 - 10 x 52) = £19,480 (no income + # tax; capital of £10,000 gives no tariff income). Guarantee Credit: none + # (above 332.95 x 52 = £17,313.40). Savings Credit: maximum + # 0.6 x (332.95 - 301.22) x 52 = £989.98, less 0.4 x (19,480 - 17,313.40) + # = £866.64, so £123.34. + # CTC: £545 + £3,455 = £4,000, not 4,000 - 0.41 x 205 = £3,915.95. + # SPC Regs 2002 reg 6(6)(d) and (11) give no Schedule IIA child amount to + # a person awarded a tax credit; it is entered because the model pays it + # (#2061). The CTC family element has been £545 since 2011-12; it was + # entered when the model uprated it, and since #1925 the model's own value + # is the same. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + grandparent_1: + age: 80 + is_claimant_or_partner: true + state_pension: 9_000 + employment_income: 3_000 + savings_interest_income: 500 + child_tax_credit_reported: 1 + grandparent_2: + age: 80 + is_claimant_or_partner: true + state_pension: 8_000 + grandchild: + age: 10 + is_claimant_or_partner: false + benunits: + benunit: + members: [grandparent_1, grandparent_2, grandchild] + would_claim_uc: false + pension_credit_earnings: 2_480 # 3,000 - 10 x 52 + child_minimum_guarantee_addition: 0 + CTC_family_element: 545 + households: + household: + members: [grandparent_1, grandparent_2, grandchild] + savings: 10_000 + output: + guarantee_credit: 0 + savings_credit: 123.34 + tax_credits_applicable_income: 0 + child_tax_credit: 4_000 + +- name: The passport does not bring a family under the childcare gross income limit + # England's two-year-old offer: a child whose parent is entitled to "child + # tax credit where that parent is not also entitled to working tax credit + # ... and has an annual gross income not exceeding £16,190", or to "the + # guaranteed element of state pension credit" (SI 2014/2147 reg 1(2), + # "eligible child", as at 1 September 2024). The previous case's couple, + # with a grandchild aged 2: their CTC is passported through the savings + # credit, but their gross income is £20,200 and they have no Guarantee + # Credit, so the child is not eligible through either route. + # CTC: the family element needs a child "born before 6th April 2017" (CTC + # Regs 2002 reg 7(2)(a)), so it is nil here (entered, as the model pays + # it: #2050); the individual element is £3,455 (reg 7(4)(c)). + period: 2024 + absolute_error_margin: 0.01 + input: + people: + grandparent_1: + age: 80 + is_claimant_or_partner: true + state_pension: 9_000 + employment_income: 3_000 + savings_interest_income: 500 + child_tax_credit_reported: 1 + grandparent_2: + age: 80 + is_claimant_or_partner: true + state_pension: 8_000 + grandchild: + age: 2 + is_claimant_or_partner: false + benunits: + benunit: + members: [grandparent_1, grandparent_2, grandchild] + would_claim_uc: false + pension_credit_earnings: 2_480 + child_minimum_guarantee_addition: 0 + CTC_family_element: 0 + working_tax_credit_run_on: false + extended_childcare_entitlement_eligible: false + households: + household: + members: [grandparent_1, grandparent_2, grandchild] + savings: 10_000 + country: ENGLAND + output: + guarantee_credit: 0 + savings_credit: 123.34 + tax_credits_applicable_income: 0 + tax_credits_current_year_income: 20_200 + child_tax_credit: 3_455 + meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false + targeted_childcare_entitlement_eligible: false + +- name: A mixed-age couple without the SI 2019/37 saving cannot get Pension Credit, so the income test applies + # SPCA 2002 s.4(1A) (from 15 May 2019): no Pension Credit where the other + # member of the couple is under the qualifying age. This couple reports + # neither Pension Credit nor Housing Benefit, so they have no SI 2019/37 + # art. 4 saving; the next case has it. The older partner is 70 and 9 months + # at 6 October 2024, so born on 6 January 1954, early enough to have reached + # the qualifying age (on 6 May 2019) before the saving's 14 May 2019 date; + # it is the missing entitlement that denies the saving. A couple both over + # that age with this income would get Guarantee Credit with the passport. + # The older partner works 20 hours, which meets the WTC hours condition for + # a joint claim with no child: "aged at least 60 and undertakes work for not + # less than 16 hours per week" (WTC Regs 2002 reg 4(1), Second condition, + # second variation). + # Tax credit income: (6,000 + 300 - 300) + 2,600 = £8,600. WTC maximum: £2,435 + £2,500 + # = £4,935, less 0.41 x (8,600 - 7,955) = £264.45, so £4,670.55. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + months_since_last_birthday: 9 + is_claimant_or_partner: true + state_pension: 6_000 + savings_interest_income: 300 + employment_income: 2_600 + weekly_hours: 20 + working_tax_credit_reported: 1 + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + households: + household: + members: [older, younger] + savings: 10_000 + output: + has_mixed_age_couple_pension_credit_saving: false + pension_credit: 0 + tax_credits_applicable_income: 8_600 + working_tax_credit: 4_670.55 + +- name: A mixed-age couple who keep Pension Credit under the SI 2019/37 saving get the passport + # The couple of the previous case (the older partner born on 6 January + # 1954, so over the qualifying age by 6 May 2019), but entitled to Pension + # Credit as that couple on 14 May 2019 and on every day since, so s.4(1A) + # does not apply to them (SI 2019/37 art. 4(1)-(2)). The older partner is + # entitled to Pension Credit, and TCA 2002 s.7(2) lifts the income test for + # the joint claim while "either of the persons" is entitled to it. + # Passported WTC: £2,435 + £2,500 = £4,935. Pension Credit income: earnings + # of 2,600 - 10 x 52 = £2,080 (SPC Regs 2002 Sch VI para 5(b), a claimant + # with a partner), plus £6,000 and £4,935, is £13,015, against a couple's + # guarantee of 332.95 x 52 = £17,313.40. So Guarantee Credit is £4,298.40 + # and the passport holds. No savings credit: neither partner "attained + # pensionable age before 6 April 2016" (SPCA 2002 s.3(1)). + period: 2024 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + months_since_last_birthday: 9 + is_claimant_or_partner: true + state_pension: 6_000 + savings_interest_income: 300 + employment_income: 2_600 + weekly_hours: 20 + working_tax_credit_reported: 1 + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + has_mixed_age_couple_pension_credit_saving: true + pension_credit_earnings: 2_080 # 2,600 - 10 x 52 + households: + household: + members: [older, younger] + savings: 10_000 + output: + tax_credits_current_year_income: 8_600 + tax_credits_applicable_income: 0 + working_tax_credit: 4_935 + savings_credit: 0 + pension_credit: 4_298.40 + +- name: With no tax credit awards in payment, Pension Credit still lifts the income test + # Tax credits ended on 5 April 2025, so no award is paid in 2025-26 and + # working tax credit is nil either way. The standard minimum guarantee for + # a single claimant is £227.10 a week (SPC Regs 2002 reg 6(1)(b), as + # substituted by SI 2025/295 art. 29(2)(b)); it was entered when the + # model had £221.85 (#1937), and since #1925 the model's own value is the + # same. Guarantee Credit: + # 227.10 x 52 - (2,340 + 6,000) = 11,809.20 - 8,340 = £3,469.20. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 71 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 6_000 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + standard_minimum_guarantee: 11_809.20 + households: + household: + members: [pensioner] + savings: 10_000 + output: + working_tax_credit: 0 + pension_credit: 3_469.20 + tax_credits_applicable_income: 0 + +- name: Without the Pension Credit passport, the first case is income-tested + # The first case's household with gov.dwp.tax_credits.means_test + # .pension_credit_passport switched off: WTC £2,170.55, and Pension Credit + # 11,343.80 - (2,340 + 6,000 + 2,170.55) = £833.25. + period: 2024 + absolute_error_margin: 0.01 + input: + gov.dwp.tax_credits.means_test.pension_credit_passport: false + people: + pensioner: + age: 70 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 20 + state_pension: 6_000 + savings_interest_income: 300 + working_tax_credit_reported: 1 + benunits: + benunit: + members: [pensioner] + pension_credit_earnings: 2_340 + households: + household: + members: [pensioner] + savings: 10_000 + output: + tax_credits_applicable_income: 8_600 + working_tax_credit: 2_170.55 + pension_credit: 833.25 diff --git a/policyengine_uk/tests/test_tax_credits_pension_credit_passport_properties.py b/policyengine_uk/tests/test_tax_credits_pension_credit_passport_properties.py new file mode 100644 index 0000000000..85764ec416 --- /dev/null +++ b/policyengine_uk/tests/test_tax_credits_pension_credit_passport_properties.py @@ -0,0 +1,392 @@ +"""State Pension Credit lifts the tax credit income test. + +TCA 2002 s.7(2) lifts the income test "for so long as the person, or either +of the persons, is entitled to" a benefit prescribed by SI 2002/2008 reg 4(1), +which includes "state pension credit" (reg 4(1)(d)): the guarantee credit, +the savings credit or both (SPCA 2002 s.1(3)). Working tax credit is Pension +Credit income (SPCA 2002 s.15(1)(b)), so the passport is decided on the +Pension Credit payable with the passported award counted. The model takes +that from a branch in which the income test is lifted. + +Properties, for any family: + +1. Differential: the passport holds exactly when an independent simulation, + with the tax credit income test lifted by input, pays Pension Credit. + Otherwise the applicable income is what it is with the Pension Credit + passport switched off. (This checks the branch against the same rule + computed outside the formula, so it catches stale or leaked caches; the + rule itself is checked against the law by the YAML cases.) +2. Consistency: where Pension Credit gives the passport, the simulation's own + tax credits and Pension Credit equal the passported simulation's. Where it + does not, they equal the values with the passport switched off. +3. Lifting the income test never lowers a tax credit award. +4. Pension Credit in payment without the passport happens only where the + passported working tax credit would be larger than the income-tested one + and would remove Pension Credit; a family with no working tax credit is + passported whenever it gets Pension Credit. +5. The results do not depend on whether Pension Credit, the income test or + household net income is requested first, and the branch is removed + afterwards. +6. In a year with no tax credit awards, the income test is lifted exactly + when Pension Credit is payable, with no dependency cycle. +7. The targeted childcare tax credit criteria are met only where the gross + (pre-passport) income is within the £16,190 limit. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Microsimulation, Simulation +from policyengine_uk.variables.gov.dwp.tax_credits_applicable_income import ( + PENSION_CREDIT_PASSPORT_BRANCH, + pension_credit_with_passported_tax_credits, +) + +ACTIVE_YEAR = 2024 # 2024-25, the last year of tax credit awards +INACTIVE_YEAR = 2025 +PASSPORT_OFF = {"gov.dwp.tax_credits.means_test.pension_credit_passport": False} +OUTPUTS = [ + "tax_credits_applicable_income", + "working_tax_credit", + "child_tax_credit", + "pension_credit", +] + + +@st.composite +def adults(draw, partner): + age = draw(st.integers(40 if partner else 55, 90)) + earnings = draw(st.sampled_from([0, 0, 2_600, 6_000, 12_000])) + return { + "age": age, + "employment_income": earnings, + "weekly_hours": draw(st.sampled_from([16, 20, 30])) if earnings else 0, + "state_pension": ( + draw(st.sampled_from([0, 4_000, 6_000, 9_000, 11_500])) if age >= 66 else 0 + ), + "private_pension_income": draw(st.sampled_from([0, 0, 1_500, 5_000])), + "savings_interest_income": draw(st.sampled_from([0, 0, 500])), + "working_tax_credit_reported": draw(st.sampled_from([0, 1])), + "child_tax_credit_reported": draw(st.sampled_from([0, 1])), + # Income-related ESA ends at State Pension age. + "esa_income_reported": ( + draw(st.sampled_from([0, 0, 0, 0, 3_000])) if age < 66 else 0 + ), + } + + +@st.composite +def families(draw): + members = [draw(adults(partner=False))] + if draw(st.booleans()): + members.append(draw(adults(partner=True))) + children = [ + {"age": draw(st.integers(0, 15))} for _ in range(draw(st.integers(0, 2))) + ] + benunit = { + "would_claim_pc": draw(st.sampled_from([True, True, True, False])), + # Whether a mixed-age couple keeps Pension Credit under the SI 2019/37 + # art. 4 saving (no effect on any other family). + "has_mixed_age_couple_pension_credit_saving": draw(st.booleans()), + } + household = {"savings": draw(st.sampled_from([0, 5_000, 12_000, 20_000]))} + return members, children, benunit, household + + +def situation(families, year, benunit_inputs=None): + people, benunits, households = {}, {}, {} + for i, (members, children, benunit, household) in enumerate(families): + names = [] + for j, inputs in enumerate(members): + name = f"a{i}_{j}" + people[name] = {k: {year: v} for k, v in inputs.items()} + people[name]["is_claimant_or_partner"] = {year: True} + names.append(name) + for j, inputs in enumerate(children): + name = f"c{i}_{j}" + people[name] = {k: {year: v} for k, v in inputs.items()} + people[name]["is_claimant_or_partner"] = {year: False} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": {year: False}, + **{k: {year: v} for k, v in benunit.items()}, + **{k: {year: v} for k, v in (benunit_inputs or {}).items()}, + } + households[f"h{i}"] = { + "members": names, + **{k: {year: v} for k, v in household.items()}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def no_passport_branch_left(sim): + """No passport branch remains, at any depth of branching.""" + for name, branch in sim.branches.items(): + if name.startswith(PENSION_CREDIT_PASSPORT_BRANCH): + return False + if not no_passport_branch_left(branch): + return False + return True + + +def calculate(sim, year, order=OUTPUTS): + values = {variable: sim.calculate(variable, year) for variable in order} + assert no_passport_branch_left(sim) + return values + + +SETTINGS = settings( + max_examples=20, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +FAMILIES = st.lists(families(), min_size=1, max_size=6) + +# The YAML cases, run on every invocation: random draws seldom raise CTC or +# hit the third row. Pension Credit earnings are net of SPC Regs Sch VI +# para 5; the CTC family element and the Sch IIA child amount are entered at +# their statutory values (see the YAML file). +_PENSIONER = { + "age": 70, + "employment_income": 2_600, + "weekly_hours": 20, + "savings_interest_income": 300, + "working_tax_credit_reported": 1, +} +YAML_FAMILIES = [ + # Guarantee Credit with WTC: passported. + ( + [{**_PENSIONER, "state_pension": 6_000}], + [], + {"pension_credit_earnings": 2_340}, + {"savings": 10_000}, + ), + # Pension Credit only on the income-tested award: the third row. + ( + [{**_PENSIONER, "state_pension": 6_600}], + [], + {"pension_credit_earnings": 2_340}, + {"savings": 10_000}, + ), + # Savings Credit alone, with CTC raised by the passport. + ( + [ + { + "age": 80, + "state_pension": 9_000, + "employment_income": 3_000, + "savings_interest_income": 500, + "child_tax_credit_reported": 1, + }, + {"age": 80, "state_pension": 8_000}, + ], + [{"age": 10}], + { + "pension_credit_earnings": 2_480, + "child_minimum_guarantee_addition": 0, + "CTC_family_element": 545, + }, + {"savings": 10_000}, + ), + # A mixed-age couple without the SI 2019/37 saving: no Pension Credit, no + # passport. The older partner was born on 6 January 1954, early enough + # for the saving. + ( + [ + {**_PENSIONER, "state_pension": 6_000, "months_since_last_birthday": 9}, + {"age": 40}, + ], + [], + {"has_mixed_age_couple_pension_credit_saving": False}, + {"savings": 10_000}, + ), + # The same couple with the saving: Guarantee Credit, passported. + ( + [ + {**_PENSIONER, "state_pension": 6_000, "months_since_last_birthday": 9}, + {"age": 40}, + ], + [], + { + "has_mixed_age_couple_pension_credit_saving": True, + "pension_credit_earnings": 2_080, + }, + {"savings": 10_000}, + ), +] + + +@SETTINGS +@given(FAMILIES) +@example(YAML_FAMILIES) +def test_passport_matches_an_independent_passported_simulation(drawn): + sim = Simulation(situation=situation(drawn, ACTIVE_YEAR)) + on = calculate(sim, ACTIVE_YEAR) + off = calculate( + Simulation(situation=situation(drawn, ACTIVE_YEAR), reform=PASSPORT_OFF), + ACTIVE_YEAR, + ) + lifted = calculate( + Simulation( + situation=situation( + drawn, + ACTIVE_YEAR, + benunit_inputs={"tax_credits_applicable_income": 0}, + ) + ), + ACTIVE_YEAR, + ) + passported = lifted["pension_credit"] > 0 + # 1. Differential. + expected = np.where(passported, 0, off["tax_credits_applicable_income"]) + assert np.allclose(on["tax_credits_applicable_income"], expected), drawn + # 2. Consistency. + for variable in OUTPUTS[1:]: + expected = np.where(passported, lifted[variable], off[variable]) + assert np.allclose(on[variable], expected, atol=0.01), (variable, drawn) + # 3. Lifting the test never lowers an award. + for variable in ["working_tax_credit", "child_tax_credit"]: + assert (on[variable] >= off[variable] - 0.01).all(), (variable, drawn) + # 4. Pension Credit without the passport needs a passported WTC award + # larger than the income-tested one (which then removes Pension Credit), + # so a family with no WTC is passported whenever it gets Pension Credit. + unpassported_pc = (on["pension_credit"] > 0) & ~passported + assert ( + lifted["working_tax_credit"][unpassported_pc] + > on["working_tax_credit"][unpassported_pc] + ).all(), drawn + # 7. The childcare tax credit criteria test gross income, not the nil + # income of a passported award. + gross = sim.calculate("tax_credits_current_year_income", ACTIVE_YEAR) + limit = sim.tax_benefit_system.parameters( + ACTIVE_YEAR + ).gov.dfe.targeted_childcare_entitlement.income_limit.tax_credits + for criterion in [ + "meets_child_tax_credit_criteria_for_targeted_childcare_entitlement", + "meets_working_tax_credit_criteria_for_targeted_childcare_entitlement", + ]: + met = sim.calculate(criterion, ACTIVE_YEAR) + assert (gross[met] <= limit).all(), (criterion, drawn) + + +@SETTINGS +@given(FAMILIES) +@example(YAML_FAMILIES) +def test_results_do_not_depend_on_request_order(drawn): + forward = calculate( + Simulation(situation=situation(drawn, ACTIVE_YEAR)), ACTIVE_YEAR + ) + backward = calculate( + Simulation(situation=situation(drawn, ACTIVE_YEAR)), + ACTIVE_YEAR, + order=list(reversed(OUTPUTS)), + ) + household_first = calculate( + Simulation(situation=situation(drawn, ACTIVE_YEAR)), + ACTIVE_YEAR, + order=["household_net_income"] + OUTPUTS, + ) + for variable in OUTPUTS: + assert np.allclose(forward[variable], backward[variable]), (variable, drawn) + assert np.allclose(forward[variable], household_first[variable]), ( + variable, + drawn, + ) + + +@SETTINGS +@given(FAMILIES) +@example(YAML_FAMILIES) +def test_without_awards_pension_credit_alone_decides_the_passport(drawn): + on = calculate(Simulation(situation=situation(drawn, INACTIVE_YEAR)), INACTIVE_YEAR) + off = calculate( + Simulation(situation=situation(drawn, INACTIVE_YEAR), reform=PASSPORT_OFF), + INACTIVE_YEAR, + ) + assert (on["working_tax_credit"] == 0).all() + assert (on["child_tax_credit"] == 0).all() + assert np.allclose(on["pension_credit"], off["pension_credit"]), drawn + expected = np.where( + on["pension_credit"] > 0, 0, off["tax_credits_applicable_income"] + ) + assert np.allclose(on["tax_credits_applicable_income"], expected), drawn + + +def test_passport_works_in_traced_and_nested_branch_simulations(): + """The branch runs under tracing and inside another branch. + + The household is the YAML's first case (Guarantee Credit with Working Tax + Credit). The marginal tax rate calculates household income in its own + branch, so the passport branch is nested there. + """ + year = ACTIVE_YEAR + family = ( + [ + { + "age": 70, + "employment_income": 2_600, + "weekly_hours": 20, + "state_pension": 6_000, + "working_tax_credit_reported": 1, + } + ], + [], + {"pension_credit_earnings": 2_340}, + {}, + ) + for trace in [False, True]: + sim = Simulation(situation=situation([family], year), trace=trace) + assert sim.calculate("tax_credits_applicable_income", year)[0] == 0 + assert np.isclose(sim.calculate("working_tax_credit", year)[0], 2_435) + assert np.isclose(sim.calculate("pension_credit", year)[0], 568.80) + mtr = sim.calculate("marginal_tax_rate", year)[0] + assert np.isfinite(mtr) + assert no_passport_branch_left(sim) + + +def test_passport_in_a_microsimulation_is_unweighted(): + """In a microsimulation the branch's Pension Credit is a plain array. + + A top-level calculate on a microsimulation returns a weighted series; + reading the branch through its population does not. + """ + year = ACTIVE_YEAR + micro = Microsimulation(situation=situation(YAML_FAMILIES, year)) + plain = Simulation(situation=situation(YAML_FAMILIES, year)) + for variable in OUTPUTS: + assert np.allclose( + np.asarray(micro.calculate(variable, year)), + plain.calculate(variable, year), + ), variable + result = pension_credit_with_passported_tax_credits( + micro.populations["benunit"], year, micro.tax_benefit_system.parameters + ) + assert type(result) is np.ndarray + assert no_passport_branch_left(micro) + + +def test_yaml_families_reach_every_case(): + """The pinned examples are not vacuous: they hit each row of the rule.""" + year = ACTIVE_YEAR + on = calculate(Simulation(situation=situation(YAML_FAMILIES, year)), year) + off = calculate( + Simulation(situation=situation(YAML_FAMILIES, year), reform=PASSPORT_OFF), + year, + ) + passported = (on["tax_credits_applicable_income"] == 0) & ( + off["tax_credits_applicable_income"] > 0 + ) + assert passported.tolist() == [True, False, True, False, True] + # The passport raises WTC (first and fifth) and CTC (third). + assert on["working_tax_credit"][0] > off["working_tax_credit"][0] + assert on["child_tax_credit"][2] > off["child_tax_credit"][2] + assert on["working_tax_credit"][4] > off["working_tax_credit"][4] + # The third row: Pension Credit paid without the passport. + assert on["pension_credit"][1] > 0 and not passported[1] + # The mixed-age couple gets Pension Credit, and so the passport, only + # under the saving. + assert on["pension_credit"][3] == 0 + assert on["pension_credit"][4] > 0 diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.py index db25570192..6f621778ea 100644 --- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_child_tax_credit_criteria_for_targeted_childcare_entitlement.py @@ -15,10 +15,13 @@ def formula(benunit, period, parameters): p = parameters(period).gov.dfe.targeted_childcare_entitlement child_tax_credit = benunit("child_tax_credit", period) > 0 working_tax_credit = benunit("working_tax_credit", period) > 0 - applicable_income = benunit("tax_credits_applicable_income", period) + # "an annual gross income not exceeding £16,190" (SI 2014/2147 reg + # 1(2), "eligible child"): the income before TCA 2002 s.7(2) lifts the + # tax credit income test, not the nil income the test then applies to. + gross_income = benunit("tax_credits_current_year_income", period) return ( child_tax_credit & ~working_tax_credit - & (applicable_income <= p.income_limit.tax_credits) + & (gross_income <= p.income_limit.tax_credits) ) diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.py index 52c475e7d9..dd201eaad5 100644 --- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_working_tax_credit_criteria_for_targeted_childcare_entitlement.py @@ -14,6 +14,11 @@ class meets_working_tax_credit_criteria_for_targeted_childcare_entitlement(Varia def formula(benunit, period, parameters): p = parameters(period).gov.dfe.targeted_childcare_entitlement working_tax_credit = benunit("working_tax_credit", period) > 0 - applicable_income = benunit("tax_credits_applicable_income", period) + # "an award which is based on an annual income not exceeding £16,190" + # (SI 2014/2147 reg 1(2), "eligible child"). The model reads this as + # the claim's income before TCA 2002 s.7(2) lifts the income test; + # the nil income a passported award is tested on would let any + # passported award through, whatever the family's income. + income = benunit("tax_credits_current_year_income", period) - return working_tax_credit & (applicable_income <= p.income_limit.tax_credits) + return working_tax_credit & (income <= p.income_limit.tax_credits) diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py index 389e51840d..4a4ffa7cd9 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -1,54 +1,96 @@ from policyengine_uk.model_api import * +# The branch in which tax credits are paid without the income test. +PENSION_CREDIT_PASSPORT_BRANCH = "tax_credits_pension_credit_passport" + + +def pension_credit_with_passported_tax_credits(benunit, period, parameters): + """Pension Credit payable alongside tax credits paid without the income test. + + Working tax credit is Pension Credit income (SPCA 2002 s.15(1)(b)), so + Pension Credit depends on the tax credit award, and TCA 2002 s.7(2) makes + the award depend on Pension Credit. The passport is consistent when + Pension Credit stays payable once the passported award is counted. This + calculates Pension Credit in a branch in which the income test is lifted: + where it is payable there, the passport holds, and the simulation's own + award and Pension Credit then equal the branch's. + + In law Pension Credit never rises with working tax credit: the guarantee + credit falls by the whole of it (s.2(2)), and it is not qualifying income + for the savings credit (SPC Regs 2002 reg 9(a)). So where Pension Credit + is payable only on the income-tested award, no consistent state exists: + lifting the test would end Pension Credit, which would restore the test. + The income test then stays, with the Pension Credit calculated on that + award. (The model's savings credit counts working tax credit as + qualifying income, so it can rise with the award; where both states are + then consistent, the passport is taken.) + + "Entitled" is read as payable: at income exactly equal to the minimum + guarantee, where no savings credit is payable, a nil guarantee credit + gives no passport. + + In years with no tax credit awards, working tax credit is nil whether or + not the test applies, so Pension Credit is read directly. + """ + if not parameters(period).gov.dwp.tax_credits.active: + return benunit("pension_credit", period) + simulation = benunit.simulation + # get_branch returns an existing branch of that name, or this simulation + # if it has that name, so take a name in use by neither. + name = PENSION_CREDIT_PASSPORT_BRANCH + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + branch.set_input("tax_credits_applicable_income", period, benunit.empty_array()) + # Through the population, so a microsimulation returns an + # unweighted array. + return branch.populations[benunit.entity.key]("pension_credit", period) + finally: + simulation.branches.pop(name, None) + class tax_credits_applicable_income(Variable): value_type = float entity = BenUnit label = "Applicable income for Tax Credits" + documentation = ( + "The income the tax credit income test applies to: the current year " + "income, or nil while the claimant or either joint claimant is " + "entitled to a benefit prescribed for TCA 2002 s.7(2)." + ) definition_period = YEAR unit = GBP reference = ( - "The Tax Credits (Definition and Calculation of Income) Regulations 2002 s. 3" + "https://www.legislation.gov.uk/ukpga/2002/21/section/7", + "https://www.legislation.gov.uk/uksi/2002/2008/regulation/4", ) def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (TCA 2002 s.7); dropping dependants' own - # income is a follow-up. Anyone else in the benefit unit does not count. - person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_qualifying_young_person_for_child_tax_credit", period - ) - TC = parameters(period).gov.dwp.tax_credits - STEP_1_COMPONENTS = [ - "private_pension_income", - "savings_interest_income", - "dividend_income", - "property_income_after_finance_costs", - ] - income = add_for_members(benunit, period, STEP_1_COMPONENTS, members) - income = max_(income - TC.means_test.non_earned_disregard, 0) - STEP_2_COMPONENTS = [ - "employment_income", - "self_employment_income", - "social_security_income", - "miscellaneous_income", - ] - bi = parameters(period).gov.contrib.ubi_center.basic_income - if bi.interactions.include_in_means_tests: - STEP_2_COMPONENTS.append("basic_income") - income += add_for_members(benunit, period, STEP_2_COMPONENTS, members) - # TCA 2002 s.7(2) and SI 2002/2008 reg 4: no income test while "the + income = benunit("tax_credits_current_year_income", period) + # TCA 2002 s.7(2) and SI 2002/2008 reg 4(1): no income test while "the # person, or either of the persons" claiming is entitled to Income # Support, income-based JSA or income-related ESA. That is the claimant's - # or partner's award, not another member's. + # or partner's award, not another member's. The reg 4(1)(a) and (b) + # exceptions, Income Support due only under IS Regs 1987 reg 6(2) and + # (3) (the lone parent run-on, omitted in Great Britain from 25 October + # 2004 by SI 2003/1589 reg 2(a)(i)) and the Northern Ireland + # equivalent, have no effect: the model has no Income Support run-on. + # Reg 4(2), which keeps the test for working tax credit in the + # four-week run-on of WTC Regs 2002 reg 7D, is not modelled: a + # whole-year award has no run-on period. EXEMPT_BENEFITS = [ "income_support", "claimant_or_partner_esa_income", "claimant_or_partner_jsa_income", ] - on_exempt_benefits = ( - add_for_members(benunit, period, EXEMPT_BENEFITS, members) > 0 - ) + on_exempt_benefits = add(benunit, period, EXEMPT_BENEFITS) > 0 + # Reg 4(1)(d): State Pension Credit, which is the guarantee credit, + # the savings credit or both (SPCA 2002 s.1(3)). A Pension Credit + # award covers the claimant and any partner. + if parameters(period).gov.dwp.tax_credits.means_test.pension_credit_passport: + on_exempt_benefits = on_exempt_benefits | ( + pension_credit_with_passported_tax_credits(benunit, period, parameters) + > 0 + ) return income * ~on_exempt_benefits diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py new file mode 100644 index 0000000000..f4c7bf0564 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py @@ -0,0 +1,51 @@ +from policyengine_uk.model_api import * + + +class tax_credits_current_year_income(Variable): + value_type = float + entity = BenUnit + label = "Current year income for Tax Credits" + documentation = ( + "The claimant's, or joint claimants', income for the tax year " + "(TCA 2002 s.7(4); SI 2002/2006 reg 3), before TCA 2002 s.7(2) lifts " + "the income test for a claimant on a prescribed benefit. The model " + "takes it as the relevant income (s.7(3)). The income test reads " + "tax_credits_applicable_income, which is nil while the test is " + "lifted; the targeted childcare criteria read this gross figure." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/21/section/7", + "https://www.legislation.gov.uk/uksi/2002/2006/regulation/3", + ) + + def formula(benunit, period, parameters): + # Members whose income counts: the claimant and partner and, as the model did + # before, the programme's own children or young persons. The regulations count + # only the claimant's and partner's (TCA 2002 s.7); dropping dependants' own + # income is a follow-up. Anyone else in the benefit unit does not count. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_qualifying_young_person_for_child_tax_credit", period + ) + TC = parameters(period).gov.dwp.tax_credits + STEP_1_COMPONENTS = [ + "private_pension_income", + "savings_interest_income", + "dividend_income", + "property_income_after_finance_costs", + ] + income = add_for_members(benunit, period, STEP_1_COMPONENTS, members) + income = max_(income - TC.means_test.non_earned_disregard, 0) + STEP_2_COMPONENTS = [ + "employment_income", + "self_employment_income", + "social_security_income", + "miscellaneous_income", + ] + bi = parameters(period).gov.contrib.ubi_center.basic_income + if bi.interactions.include_in_means_tests: + STEP_2_COMPONENTS.append("basic_income") + income += add_for_members(benunit, period, STEP_2_COMPONENTS, members) + return income diff --git a/policyengine_uk/variables/gov/dwp/wtc_entitlement.py b/policyengine_uk/variables/gov/dwp/wtc_entitlement.py index ff4eeabdc6..3a0a606c99 100644 --- a/policyengine_uk/variables/gov/dwp/wtc_entitlement.py +++ b/policyengine_uk/variables/gov/dwp/wtc_entitlement.py @@ -10,6 +10,12 @@ class wtc_entitlement(Variable): defined_for = "is_WTC_eligible" def formula(benunit, period, parameters): + # With no awards in payment, return before reading the income test: + # Pension Credit counts working tax credit (SPCA 2002 s.15(1)(b)), and + # the tax credit income test reads Pension Credit directly in those + # years (tax_credits_applicable_income). + if not parameters(period).gov.dwp.tax_credits.active: + return benunit.empty_array() return where( benunit("tax_credits", period) > 0, benunit("working_tax_credit_pre_minimum", period),