diff --git a/changelog.d/pc-earnings-disregard.fixed.md b/changelog.d/pc-earnings-disregard.fixed.md new file mode 100644 index 0000000000..53ad66d2e6 --- /dev/null +++ b/changelog.d/pc-earnings-disregard.fixed.md @@ -0,0 +1 @@ +Apply the Pension Credit earnings disregards of SPC Regs 2002 Schedule VI: 20 pounds a week for lone parents, carers and claimants or partners on a listed disability benefit (including working tax credit with a disability element) or certified blind, otherwise 5 pounds (single) or 10 pounds (couple), capped at net earnings. Employed earnings are net of the income tax they add; self-employed earnings, in Pension Credit income too, are net of the notional tax and contributions of the Computation of Earnings Regulations 1996 reg. 14. The Housing Benefit savings-credit-only route now deducts only the higher Housing Benefit disregards. diff --git a/changelog.d/pc-earnings-tax-reuse.changed.md b/changelog.d/pc-earnings-tax-reuse.changed.md new file mode 100644 index 0000000000..601aa2a6d9 --- /dev/null +++ b/changelog.d/pc-earnings-tax-reuse.changed.md @@ -0,0 +1 @@ +- Reuse Pension Credit earnings tax calculations when checking Universal Credit housing contribution exemptions. diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml new file mode 100644 index 0000000000..bc51f73542 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly earnings disregarded from Pension Credit income for a lone parent, a carer, or a claimant or partner on a listed disability benefit or certified blind. It is the most that can be disregarded however many conditions are met. +values: + 2003-10-06: 20 +metadata: + unit: currency-GBP + period: week + label: Pension Credit higher earnings disregard + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI paras. 1, 3, 4 and 4A + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml new file mode 100644 index 0000000000..8e31a28658 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml @@ -0,0 +1,19 @@ +description: > + Benefit-unit awards whose receipt qualifies the benefit unit for the higher + Pension Credit earnings disregard (Sch. VI para. 4(1)(a)(vii), employment + and support allowance, from 27 October 2008). The model holds income-related + employment and support allowance on the benefit unit, as the claimant's + award; contributory employment and support allowance is a person-level + benefit in `disability_benefits`. +metadata: + label: Pension Credit higher earnings disregard benefit-unit disability benefits + unit: list + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 4(1)(a)(vii) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/4 + - title: The Employment and Support Allowance (Consequential Provisions) (No. 2) Regulations 2008 reg. 4(10)(a)(ii) + href: https://www.legislation.gov.uk/uksi/2008/1554/regulation/4 +values: + 2003-10-06: [] + 2008-10-27: + - esa_income diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml new file mode 100644 index 0000000000..6aa826b95b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml @@ -0,0 +1,46 @@ +description: > + Person-level benefits whose receipt by the claimant or partner qualifies the + benefit unit for the higher Pension Credit earnings disregard (Sch. VI para. + 4(1)(a)). Income-related employment and support allowance, a benefit-unit + award, is in `disability_benefit_unit_benefits`. + `incapacity_benefit` stands for long-term incapacity benefit (para. + 4(1)(a)(i)); the model does not separate the short-term rates. Not modelled: + the war pensions mobility supplement (v), which has no variable; pension age + disability payment (iiia, from 21 October 2024), Scottish adult disability + living allowance (iva, from 21 March 2025) and adult disability payment + (viiia, from 21 March 2022), which have no separate variables. Working tax + credit with its disability or severe disability element (vi) is a + benefit-unit award, tested in the formula of + `pension_credit_earnings_disregard`. +metadata: + label: Pension Credit higher earnings disregard disability benefits + unit: list + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 4(1)(a) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/4 + - title: The Employment and Support Allowance (Consequential Provisions) (No. 2) Regulations 2008 reg. 4(10)(a)(ii) (ESA, from 27 October 2008) + href: https://www.legislation.gov.uk/uksi/2008/1554/regulation/4 + - title: The Personal Independence Payment (Supplementary Provisions and Consequential Amendments) Regulations 2013 Sch. para. 27(8)(b) (PIP, from 8 April 2013) + href: https://www.legislation.gov.uk/uksi/2013/388/schedule/paragraph/27 + - title: "The Armed Forces and Reserve Forces Compensation Scheme (Consequential Provisions: Subordinate Legislation) Order 2013 Sch. para. 23(8)(b) (AFIP, from 8 April 2013)" + href: https://www.legislation.gov.uk/uksi/2013/591/schedule/paragraph/23 +values: + 2003-10-06: + - incapacity_benefit + - sda + - attendance_allowance + - dla + 2008-10-27: + - incapacity_benefit + - sda + - attendance_allowance + - dla + - esa_contrib + 2013-04-08: + - incapacity_benefit + - sda + - attendance_allowance + - dla + - esa_contrib + - pip + - armed_forces_independence_payment diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml new file mode 100644 index 0000000000..80ffae79fe --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml @@ -0,0 +1,20 @@ +description: Weekly earnings disregarded from Pension Credit income where neither the claimant nor a partner qualifies for the higher disregard. +SINGLE: + values: + 2003-10-06: 5 + metadata: + label: Pension Credit standard earnings disregard (single) +COUPLE: + values: + 2003-10-06: 10 + metadata: + label: Pension Credit standard earnings disregard (couple) +metadata: + unit: currency-GBP + period: week + propagate_metadata_to_children: true + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 5 + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/5 + - title: The State Pension Credit Regulations 2002 reg. 17(9) + href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/17 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml index bc8af60aad..4df1327e70 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml @@ -141,8 +141,9 @@ pension_credit_assessable_capital: [20_000, 0] # 18,000 + 1,040 = 19,040, above the couple minimum guarantee of 18,889. # The non-dependant's is computed but he cannot have Pension Credit: - # 20,000 - 1,486 income tax - 594.40 National Insurance = 17,919.60. - pension_credit_income: [19_040, 17_919.60] + # 20,000 - 1,486 income tax - 594.40 National Insurance - the 5 pound + # earnings disregard (260) = 17,659.60. + pension_credit_income: [19_040, 17_659.60] guarantee_credit: [0, 0] # 1,045.20 - 0.4 x (19,040 - 18,889) = 1,045.20 - 60.40 = 984.80. savings_credit: [984.80, 0] @@ -169,9 +170,8 @@ members: [person1] # £5 a week for a single claimant with earnings (Sch 4 para 7(a)), # the same as the Pension Credit disregard (SPC Regs 2002 Sch VI - # para 5(a)); 5 x 52 = 260. Pension Credit income in the model - # disregards no earnings, so deducting it gives the Secretary of - # State's figure. + # para 5(a)); 5 x 52 = 260. The Secretary of State's figure is + # already after it, and reg 27(4) deducts only higher amounts. housing_benefit_applicable_income_disregard: 260 households: household: @@ -180,14 +180,15 @@ tenure_type: RENT_FROM_COUNCIL savings: 0 output: - # 13,000 - 86 = 12,914, as in case 1. - pension_credit_income: 12_914 - savings_credit: 718.62 + # 13,000 - 86 - the 5 pound Pension Credit earnings disregard (260). + pension_credit_income: 12_654 + # 933.82 - 0.4 x (12,654 - 12,376) = 933.82 - 111.20 = 822.62. + savings_credit: 822.62 in_receipt_of_savings_credit_only: true - # 12,914 + 718.62 - 260 = 13,372.62. - housing_benefit_applicable_income: 13_372.62 - # 7,800 - 0.65 x (13,372.62 - 13,312) = 7,800 - 39.40 = 7,760.60. - housing_benefit: 7_760.60 + # 12,654 + 822.62 = 13,476.62; nothing more is disregarded. + housing_benefit_applicable_income: 13_476.62 + # 7,800 - 0.65 x (13,476.62 - 13,312) = 7,800 - 107.00 = 7,693.00. + housing_benefit: 7_693 - name: Case 5, savings credit only lone parent in work, childcare charges and the lone parent and additional earnings disregards are deducted. period: 2026 @@ -196,7 +197,7 @@ people: person1: age: 78 - state_pension: 11_000 + state_pension: 12_500 employment_income: 7_000 # 20 hours a week, so remunerative work (reg 6) and the reg 31 # condition for deducting childcare charges are met. @@ -207,13 +208,10 @@ benunits: benunit: members: [person1, person2] - # Reg 27(4)(c)(i): the higher amount disregarded for a lone parent's - # earnings, £25 a week (Sch 4 para 2) against Pension Credit's £20 - # (SPC Regs 2002 Sch VI para 1); and (d): the additional £17.10 (Sch - # 4 para 9(1)) for a lone parent working 16 hours or more whose net - # earnings cover the disregards and childcare. Pension Credit income - # in the model disregards no earnings, so the whole of both is - # deducted: (25 + 17.10) x 52 = 2,189.20, the value #1908 gives. + # The lone parent's £25 a week (Sch 4 para 2) and the additional + # £17.10 (Sch 4 para 9(1)) for a lone parent working 16 hours or more + # whose net earnings cover the disregards and childcare: + # (25 + 17.10) x 52 = 2,189.20, the value #1908 gives. housing_benefit_applicable_income_disregard: 2_189.20 households: household: @@ -222,20 +220,57 @@ tenure_type: RENT_FROM_COUNCIL savings: 0 output: - # (18,000 - 12,570) x 0.2 = 1,086; 18,000 - 1,086 = 16,914. - pension_credit_income: 16_914 + # The lone parent's 20 pound Pension Credit disregard (SPC Regs 2002 Sch + # VI para 1): 20 x 52 = 1,040. + pension_credit_earnings_disregard: 1_040 + # (19,500 - 12,570) x 0.2 = 1,386; 19,500 - 1,386 - 1,040 = 17,074. + pension_credit_income: 17_074 # 12,376 + 3,638.96 = 16,014.96. minimum_guarantee: 16_014.96 guarantee_credit: 0 - # 933.82 - 0.4 x (16,914 - 16,014.96) = 933.82 - 359.62 = 574.20. - savings_credit: 574.20 + # 933.82 - 0.4 x (17,074 - 16,014.96) = 933.82 - 423.62 = 510.20. + savings_credit: 510.20 in_receipt_of_savings_credit_only: true # Reg 27(4)(b): 2,600 of childcare (£50 a week), below the weekly # maximum and the earnings. housing_benefit_applicable_income_childcare_element: 2_600 - # 16,914 + 574.20 - 2,600 - 2,189.20 = 12,699. - housing_benefit_savings_credit_only_income: 12_699 - housing_benefit_applicable_income: 12_699 + # Reg 27(4)(c)(i): the higher amount for a lone parent's earnings, + # 1,300 - 1,040 = 260; (d): the additional 17.10 x 52 = 889.20. + # 17,074 + 510.20 - 2,600 - 260 - 889.20 = 13,835. + housing_benefit_savings_credit_only_income: 13_835 + housing_benefit_applicable_income: 13_835 + +- name: Case 5a, the same lone parent with less State Pension, whose 20 pound Pension Credit disregard brings a guarantee credit and its passport. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 78 + state_pension: 11_000 + employment_income: 7_000 + hours_worked: 1_040 + person2: + age: 8 + childcare_expenses: 2_600 + benunits: + benunit: + members: [person1, person2] + housing_benefit_applicable_income_disregard: 2_189.20 + households: + household: + members: [person1, person2] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + # (18,000 - 12,570) x 0.2 = 1,086; 18,000 - 1,086 - 1,040 = 15,874. + pension_credit_income: 15_874 + # 16,014.96 - 15,874 = 140.96. + guarantee_credit: 140.96 + in_receipt_of_savings_credit_only: false + # The guarantee credit passports the maximum Housing Benefit: the rent. + housing_benefit: 7_800 - name: Case 6, guarantee credit and savings credit together, the award is not savings credit only and the guarantee credit passport applies. period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml index 462cdd1fca..a54bf6b739 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml @@ -160,9 +160,13 @@ is_claimant_or_partner: [true, true] # SPCA 2002 s.5: gross = 5,000 + 1,000 + 100 + 2,000 + 500 + 200 # + 104 = 8,904; deductions = 100 + 50 + 25 + 50% * 300 = 325. - pension_credit_income: 8_579 + # Sch VI para 3 (both carers): 20 pounds a week, capped at net earnings + # (claimant 1,000 - 100 tax - 100 half contributions = 800; partner + # 500 - 25 NI - 50 tax - 50 = 375; 1,175), so 1,040 is disregarded: + # 8,904 - 325 - 1,040. + pension_credit_income: 7_539 # Reg.9: remove the claimant's 100 JSA and partner's 200 ESA. - savings_credit_income: 8_279 + savings_credit_income: 7_239 # Sch.I para.4 and the 2026 carer addition: 2 * 48.15 * 52 = 5,007.60. carer_minimum_guarantee_addition: 5_007.60 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_amount_tax_credit_award.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_amount_tax_credit_award.yaml index 2b8e58ff0e..9f9456d020 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_amount_tax_credit_award.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/child_amount_tax_credit_award.yaml @@ -39,12 +39,11 @@ # credit (SPCA 2002 s.3(1)(a)). The 2018-19 and 2019-20 couples have no # income, so no savings credit either. The Housing Benefit cases' couple is # aged 80, so savings credit applies to it. The grandchild is 10 (11 in -# 2025-26), born before 6 April 2017. Pension Credit earnings are entered net -# of the SPC Regs Sch VI para 5 disregard (£10 a week for a couple), which -# the model does not apply. £10,000 of savings gives no Pension Credit tariff -# income, and £300 of interest uses up the £300 step-one disregard of tax -# credit income (SI 2002/2006 reg 3). No one pays income tax or National -# Insurance. +# 2025-26), born before 6 April 2017. Employment income is gross: the model +# applies the SPC Regs Sch VI para 5 disregard (£10 a week for a couple) +# once. £10,000 of savings gives no Pension Credit tariff income, and £300 +# of interest uses up the £300 step-one disregard of tax credit income +# (SI 2002/2006 reg 3). No one pays income tax or National Insurance. - name: A Pension Credit couple awarded CTC and WTC gets no child amount # Tax credit income: State Pension 8,000 + interest (300 - 300) + earnings @@ -83,7 +82,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_080 # 2,600 - 10 x 52 + # Gross wages are 2,600; Sch VI deducts 10 x 52, leaving 2,080. CTC_family_element: 545 households: household: @@ -125,7 +124,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_080 + # Gross wages are 2,600; Sch VI deducts 520, leaving 2,080 once. households: household: members: [grandparent_1, grandparent_2, grandchild] @@ -166,7 +165,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_080 + # As in the first case, gross wages 2,600 become 2,080 after Sch VI. households: household: members: [grandparent_1, grandparent_2, grandchild] @@ -213,7 +212,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_980 # 3,500 - 10 x 52 + # Gross wages 3,500 become 2,980 after Sch VI, even with no PC paid. households: household: members: [grandparent_1, grandparent_2, grandchild] @@ -254,7 +253,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_080 + # Gross wages 2,600 less one Sch VI disregard of 520 give 2,080. has_tax_credit_award: true households: household: @@ -407,7 +406,7 @@ benunit: members: [grandparent_1, grandparent_2, grandchild] would_claim_uc: false - pension_credit_earnings: 2_080 + # Tax-credit closure does not alter gross wages 2,600 less Sch VI 520. standard_minimum_guarantee: 18_023.20 # 346.60 x 52 would_claim_uc_at_legacy_closure: false households: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml new file mode 100644 index 0000000000..f3f11f8954 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -0,0 +1,676 @@ +# Expected values are hand-computed from SPC Regs 2002 Sch VI (paras 1, 3, 4, +# 4A and 5): weekly amounts times 52 weeks, capped at the earnings. + +- name: A single claimant with earnings has 5 pounds a week disregarded (para 5(a)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: A couple with earnings has 10 pounds a week disregarded (para 5(b)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 10 * 52 + +- name: A claimant on attendance allowance has 20 pounds a week disregarded (para 4(1)(a)(iii)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + attendance_allowance: 3000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A partner's personal independence payment qualifies the couple (para 4(1)(a)(viii)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + pip_dl: 3000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A carer satisfying Sch I para 4 has 20 pounds a week disregarded (para 3). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A lone parent has 20 pounds a week disregarded (para 1). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + child: + age: 10 + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A claimant certified blind has 20 pounds a week disregarded (para 4(1)(b)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + is_blind: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: 20 pounds is the most disregarded when both partners qualify (para 4A). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + attendance_allowance: 3000 + partner: + age: 70 + employment_income: 2000 + dla_sc: 3000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: The disregard never exceeds the earnings. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 100 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 100 + +- name: Self-employed earnings get the same disregard. + period: 2024 + input: + people: + claimant: + age: 70 + self_employment_income: 2000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: Without earnings there is nothing to disregard. + period: 2024 + input: + people: + claimant: + age: 70 + attendance_allowance: 3000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 0 + +- name: A dependant child's disability benefit does not qualify a couple for the higher disregard. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + child: + age: 10 + dla_sc: 3000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + pension_credit_earnings_disregard: 10 * 52 + +- name: Income-related ESA, a benefit-unit award, qualifies for the higher disregard. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + benunits: + benunit: + members: [claimant] + esa_income: 1000 + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: The disregard comes off Pension Credit income and raises the guarantee credit. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + state_pension: 9000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # minimum guarantee 218.15 a week in 2024-25 (SI 2024/242) + pension_credit_income: 9_000 + 2_000 - 5 * 52 + guarantee_credit: 218.15 * 52 - (9_000 + 2_000 - 5 * 52) + +- name: A taxpayer's disregard is capped at net earnings, so earnings below the disregard leave Pension Credit income unchanged (reg 17(9), 17(10)). + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 3_000 + attendance_allowance: 3_000 + employment_income: 1_040 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Income tax (16,040 - 12,570) x 20% = 694, against 486 without the + # earnings (next case), so tax on the earnings is 208 and net earnings are + # 832; the 20 pound (attendance allowance) disregard of 1,040 is capped at + # 832. + pension_credit_earnings_disregard: 832 + # 16,040 - 694 - 832, the same as with no earnings (next case). + pension_credit_income: 14_514 + +- name: The same claimant with no earnings has the same Pension Credit income. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 3_000 + attendance_allowance: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # 15,000 - (15,000 - 12,570) x 20% + pension_credit_earnings_disregard: 0 + pension_credit_income: 14_514 + +- name: A dependant's caring does not qualify a couple for the higher disregard (Sch I para 4 is the claimant's or partner's). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2_000 + partner: + age: 70 + child: + age: 17 + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + pension_credit_earnings_disregard: 10 * 52 + +- name: A Scottish taxpayer's tax on earnings is the income tax the earnings add on the Scottish schedule, so Pension Credit income does not fall with earnings. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 16_000 + attendance_allowance: 5_000 + employment_income: 1_040 + partner: + age: 70 + attendance_allowance: 5_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: SCOTLAND + output: + # The 1,040 of earnings add 210.89 of income tax (they cross into the + # Scottish intermediate band), so net earnings are 829.11 and the 20 pound + # disregard of 1,040 is capped there; income is as with no earnings (next). + income_tax: [3_016.62, 0] + pension_credit_earnings_disregard: 829.11 + pension_credit_income: 25_194.27 + +- name: The same Scottish couple with no earnings has the same Pension Credit income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 16_000 + attendance_allowance: 5_000 + partner: + age: 70 + attendance_allowance: 5_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: SCOTLAND + output: + income_tax: [2_805.73, 0] + pension_credit_earnings_disregard: 0 + pension_credit_income: 25_194.27 + +- name: Caring hours without entitlement to carer's allowance do not qualify for the 20 pound disregard (Sch VI para 3 needs Sch I para 4). + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + care_hours: 40 + is_entitled_to_carer_benefit: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: Entitlement to carer's allowance qualifies for the 20 pound disregard. + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + care_hours: 40 + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: Tax on earnings includes the tax they add on savings income, so Pension Credit income does not fall with earnings. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + savings_interest_income: 12_000 + attendance_allowance: 3_000 + employment_income: 1_040 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Non-savings tax: (12,000 + 1,040 - 12,570) x 20% = 94. + # Those taxable non-savings earnings reduce the savings starting-rate + # band to 5,000 - 470 = 4,530. Savings tax is + # (12,000 - 4,530 - 1,000 PSA) x 20% = 1,294, giving total tax 1,388. + # Without wages tax is 1,086 (next case), so employment tax is 302 and + # net earnings/disregard are 1,040 - 302 = 738, below the 1,040 cap. + # PC counts deemed capital income, not interest: 12,000 + 1,040 - 1,388 + # - 738 = 10,914. HMRC: /apply-tax-free-interest-on-savings/how-much-is-tax-free. + income_tax: 1_388 + pension_credit_employment_income_tax: 302 + pension_credit_earnings_disregard: 738 + pension_credit_income: 10_914 + +- name: The same claimant with savings and no earnings has the same Pension Credit income. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + savings_interest_income: 12_000 + attendance_allowance: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + income_tax: 1_086 + pension_credit_earnings_disregard: 0 + # 12,000 - 1,086. + pension_credit_income: 10_914 + +- name: Pension contribution relief already available without earnings does not reduce tax attributed to earnings. + period: 2024 + input: + people: + claimant: + age: 70 + private_pension_income: 15_000 + employment_income: 400 + personal_pension_contributions: 300 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Relief is available on these 300 of contributions even without wages + # (FA 2004 s.190's 3,600 basic amount, age under 75). With wages tax is + # (15,400 - 12,570 - 300) x 20% = 506; without, it is 426 (next case). + # Employment tax is 80, so net earnings/disregard are + # 400 - 80 - 300 / 2 = 170, under the 5 pound cap of 260. + income_tax: 506 + pension_credit_employment_income_tax: 80 + pension_credit_earnings_disregard: 170 + # 15,400 - 506 - 150 - 170 = 14,574; the net-earnings floor means PC + # income is 150 more than without wages (next case). + pension_credit_income: 14_574 + +- name: The same claimant with no earnings. + period: 2024 + input: + people: + claimant: + age: 70 + private_pension_income: 15_000 + personal_pension_contributions: 300 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # No wages are needed for relief on the 300 contribution below the + # FA 2004 s.190 basic amount: (15,000 - 12,570 - 300) x 20% = 426. + income_tax: 426 + pension_credit_earnings_disregard: 0 + # 15,000 - 426 - 300 / 2 = 14,424. + pension_credit_income: 14_424 + +- name: A self-employment loss is not set against wages in the disregard (CER 1996 reg 13(12)). + period: 2025 + input: + people: + claimant: + age: 70 + employment_income: 400 + self_employment_income: -8_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Net earnings are the 400 of wages; the 5 pound disregard is 260. + pension_credit_earnings_disregard: 260 + pension_credit_income: 400 - 260 + +- name: Self-employed earnings under the personal allowance carry no notional tax, and the tax they actually cause is not deducted (CER 1996 reg 14; SPC reg 17(11)). + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + self_employment_income: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # The State Pension uses all but 570 of the personal allowance, so the + # profit causes all of the (15,000 - 12,570) x 20% = 486 of income tax. + income_tax: 486 + pension_credit_self_employment_income_tax: 486 + # Notional tax at the basic rate on the profit less the personal + # allowance alone is nil; Class 4 is not payable over State Pension age. + pension_credit_self_employment_deductions: 0 + pension_credit_earnings_disregard: 260 + # 15,000 - (486 - 486 + 0) - 260. + pension_credit_income: 14_740 + +- name: Profit within the trading allowance is tax-free and fully disregarded up to the 20 pound limit. + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 3_000 + attendance_allowance: 3_000 + self_employment_income: 1_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # The 1,000 trading allowance relieves the profit, so income tax is the + # (15,000 - 12,570) x 20% = 486 on the pensions. + income_tax: 486 + pension_credit_self_employment_income_tax: 0 + pension_credit_earnings_disregard: 1_000 + # 16,000 - 486 - 1,000, the same as with no profit: 15,000 - 486. + pension_credit_income: 14_514 + +- name: A Scottish self-employed earner's notional tax is at the Scottish basic rate, with main-rate Class 4 under State Pension age. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + self_employment_income: 20_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: SCOTLAND + output: + # (20,000 - 12,570) x 20% = 1,486, not the 1,457.73 of income tax on the + # Scottish starter, basic and intermediate bands; Class 4 is + # (20,000 - 12,570) x 6% = 445.80. + income_tax: 1_457.73 + pension_credit_self_employment_deductions: 1_486 + 445.80 + +- name: Before April 2024 the notional contributions include Class 2 (CER 1996 reg 14(2)(a)). + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + self_employment_income: 20_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # (20,000 - 12,570) x 20% = 1,486; Class 4 (20,000 - 12,570) x 9% = + # 668.70; Class 2 3.45 x 52 = 179.40. + pension_credit_self_employment_deductions: 1_486 + 668.70 + 179.40 + +- name: The notional tax deducts the blind person's allowance with the personal allowance (ITA 2007 Part 3 Chapter 2). + period: 2024 + input: + people: + claimant: + age: 70 + self_employment_income: 20_000 + blind_persons_allowance: 3_070 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # (20,000 - 12,570 - 3,070) x 20%. + pension_credit_self_employment_deductions: 872 + +- name: Working tax credit with its disability element qualifies for the 20 pound disregard (Sch VI para 4(1)(a)(vi)). + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + benunits: + benunit: + members: [claimant] + WTC_disabled_element: 3_000 + working_tax_credit: 1_000 + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: The disability element counts only with working tax credit in payment. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + benunits: + benunit: + members: [claimant] + WTC_disabled_element: 3_000 + working_tax_credit: 0 + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml index a69dfec245..d98e564feb 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml @@ -20,7 +20,11 @@ savings: 11_000 output: pension_credit_deemed_income: 104 - pension_credit_income: 343 + # 1 + 2 + 4 + 8 + 32 + 64 + 128 + 104 = 343, less the Sch VI disregard: + # the 20 pound para 4(1)(a)(vii) amount (contributory ESA), capped at the + # 129 of net earnings (no tax or NI on them). + pension_credit_earnings_disregard: 129 + pension_credit_income: 214 - name: Interest and dividends are not counted directly in Pension Credit income. period: 2021 @@ -215,6 +219,8 @@ tenure_type: OWNED_OUTRIGHT output: pension_credit_earnings: 10_000 - pension_credit_income: 10_000 - # 238 x 52 = 12,376, less income of 10,000. - pension_credit: 2_376 + # 10,000 less the 5 pound a week earnings disregard (260); the earnings + # are under the personal allowance and the primary threshold. + pension_credit_income: 9_740 + # 238 x 52 = 12,376, less income of 9,740. + pension_credit: 2_636 diff --git a/policyengine_uk/tests/test_pension_credit_child_amount_tax_credit_properties.py b/policyengine_uk/tests/test_pension_credit_child_amount_tax_credit_properties.py index 87929cd78a..1ce961f531 100644 --- a/policyengine_uk/tests/test_pension_credit_child_amount_tax_credit_properties.py +++ b/policyengine_uk/tests/test_pension_credit_child_amount_tax_credit_properties.py @@ -158,8 +158,9 @@ def schedule_iia_amount(drawn, year): FAMILIES = st.lists(families(), min_size=1, max_size=6) # The YAML households (child_amount_tax_credit_award.yaml), run on every -# invocation. Pension Credit earnings are net of SPC Regs Sch VI para 5, and -# the CTC family element is entered at its statutory £545. The last two pay +# invocation. Wages are gross: the first three families' £2,600 less the +# SPC Regs Sch VI para 5 couple disregard of £10 x 52 gives £2,080 once. +# The CTC family element is entered at its statutory £545. The last two pay # rent, so the order and differential checks reach Housing Benefit; they # enter the Housing Benefit claim, which the model otherwise infers from # reported benefits across the whole simulation. @@ -170,10 +171,12 @@ def schedule_iia_amount(drawn, year): "state_pension": 4_000, "savings_interest_income": 300, } -_INPUTS = {"would_claim_uc": False, "pension_credit_earnings": 2_080} +_INPUTS = {"would_claim_uc": False} _RENTED = {"savings": 18_000, "rent": 12_000, "tenure_type": "RENT_FROM_COUNCIL"} YAML_FAMILIES = [ - # CTC and WTC with Pension Credit: passported, no child amount. + # CTC and WTC with PC: pensions 8,000 + net earnings 2,080 + passported + # WTC 4,935 = income 15,015; GC is 332.95 x 52 - 15,015 = 2,298.40. + # A tax-credit award excludes the 76.79 x 52 = 3,993.08 child amount. ( [ { @@ -187,14 +190,16 @@ def schedule_iia_amount(drawn, year): {**_INPUTS, "CTC_family_element": 545}, {"savings": 10_000}, ), - # No tax credit claim: the child amount. + # No tax credit claim: pensions 8,000 + net earnings 2,080 = 10,080. + # With the child amount, GC is 17,313.40 + 3,993.08 - 10,080 = 11,226.48. ( [_GRANDPARENT, {"age": 70, "state_pension": 4_000}], [{"age": 10}], _INPUTS, {"savings": 10_000}, ), - # WTC alone. + # WTC alone also excludes the child amount; the first family's income + # 15,015 and GC 2,298.40 are unchanged because CTC is not PC income. ( [ {**_GRANDPARENT, "working_tax_credit_reported": 1}, @@ -204,7 +209,11 @@ def schedule_iia_amount(drawn, year): _INPUTS, {"savings": 10_000}, ), - # A WTC award at a nil rate. + # Gross wages 3,500 - Sch VI 520 = 2,980, not a pre-netted earnings + # input. With pensions 17,000, income 19,980 exceeds guarantee 17,313.40. + # WTC is max(0, 4,935 - 41% x (20,500 - 7,955)) = 0, still an award. + # Without that award the child amount gives GC 21,306.48 - 19,980 = + # 1,326.48, satisfying the existing positive-award boundary assertion. ( [ { @@ -216,7 +225,7 @@ def schedule_iia_amount(drawn, year): {"age": 70, "state_pension": 8_000}, ], [{"age": 10}], - {**_INPUTS, "pension_credit_earnings": 2_980}, + _INPUTS, {"savings": 10_000}, ), # A CTC award ends the Guarantee Credit and its Housing Benefit passport. diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py new file mode 100644 index 0000000000..cad0a87ef3 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -0,0 +1,338 @@ +"""Properties of the Pension Credit earnings disregards (SPC Regs 2002 Sch VI) +and of the net earnings they apply to (regs. 17(10), 17(11), 17A and 17B). + +Invariants, over generated benefit units in England and Scotland: + +1. Statute: the disregard equals min(net earnings, 52 x w), recomputed here + from the inputs and independent simulations: + - each person's own earnings, each source floored at nil, less their + primary Class 1 contributions, half of their pension contributions and, + on self-employed profit, the notional tax and contributions of CER 1996 + reg. 14 (basic rate on the profit less the personal allowance and any + marriage allowance; main-rate Class 4 under State Pension age), floored + at nil; + - less the income tax payable on the claimant's and partner's employed + earnings, measured as the fall in their income tax when the household + is simulated again with nobody's employment income. + w is 20 if the unit is a lone-parent family, or a claimant or partner is + entitled to carer's allowance (an input here), receives a listed + disability benefit or is blind; otherwise 5 single, 10 couple. A dependent + child's DLA or caring never qualifies. +2. Bounds: 0 <= disregard <= min(gross earnings, 20 x 52). +3. Differential: Pension Credit income is the sources less tax, NI and half + of pension contributions, with the tax and contributions actually payable + on self-employed profit replaced by the notional ones, floored at 0, less + the disregard, floored at 0. +4. Monotonicity: the guarantee credit is never below its value without the + disregard (for units with nobody under State Pension age). +5. Earning more, employed or self-employed, by either partner, never raises + Pension Credit. +6. Under the disregard, Pension Credit income does not change with earnings. +7. The dated benefit lists follow the amending instruments: ESA from 27 + October 2008, PIP and AFIP from 8 April 2013. +8. The Scottish basic rate read for the notional tax is 20% in every year, + including those before the starter rate. +""" + +import numpy as np +from hypothesis import HealthCheck, assume, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import WEEKS_IN_YEAR + +YEAR = 2024 +BASIC_RATE = 0.2 +PERSONAL_ALLOWANCE = 12_570 +CLASS_4_MAIN_RATE = 0.06 +LOWER_PROFITS_LIMIT = 12_570 +UPPER_PROFITS_LIMIT = 50_270 +STATE_PENSION_AGE = 66 +SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +DISABILITY = [ + "attendance_allowance", + "dla_sc", + "pip_dl", + "incapacity_benefit", + "esa_contrib", +] + + +@st.composite +def units(draw): + country = draw(st.sampled_from(["ENGLAND", "SCOTLAND"])) + couple = draw(st.booleans()) + child = draw(st.booleans()) + people = {} + names = ["claimant"] + (["partner"] if couple else []) + for n in names: + person = { + "age": draw( + st.sampled_from([67, 72, 85] + ([64] if n == "partner" else [])) + ), + "employment_income": draw(st.sampled_from([0, 50, 400, 1_040, 3_000])), + "self_employment_income": draw( + st.sampled_from([0, 0, 1_500, 15_000, -2_000]) + ), + "state_pension": draw(st.integers(min_value=0, max_value=12_000)), + "private_pension_income": draw(st.sampled_from([0, 2_000, 6_000])), + "savings_interest_income": draw(st.sampled_from([0, 0, 3_000])), + "is_blind": draw(st.sampled_from([False, False, False, True])), + "is_entitled_to_carer_benefit": draw( + st.sampled_from([False, False, False, True]) + ), + "care_hours": draw(st.sampled_from([0, 0, 40])), + "personal_pension_contributions": draw(st.sampled_from([0, 0, 300])), + } + benefit = draw(st.sampled_from([None, None] + DISABILITY)) + if benefit: + person[benefit] = 2_000 + people[n] = person + if child: + # A dependent child's DLA or caring never qualifies the unit. + people["child"] = { + "age": 10, + "dla_sc": draw(st.sampled_from([0, 2_000])), + "is_entitled_to_carer_benefit": draw(st.booleans()), + } + return {"people": people, "country": country} + + +def simulate(case): + people = case["people"] + members = list(people) + dated = { + name: {k: {str(YEAR): v} for k, v in person.items()} + for name, person in people.items() + } + return Simulation( + situation={ + "people": dated, + "benunits": {"b": {"members": members}}, + "households": { + "h": {"members": members, "country": {str(YEAR): case["country"]}} + }, + } + ) + + +def without(case, sources): + """The case with nobody's income from each of ``sources``.""" + people = {n: dict(p) for n, p in case["people"].items()} + for person in people.values(): + for source in sources: + if source in person: + person[source] = 0 + return {"people": people, "country": case["country"]} + + +def unit_value(sim, variable): + return float(sim.calculate(variable, YEAR)[0]) + + +def person_values(sim, variable): + return np.asarray(sim.calculate(variable, YEAR, map_to="person"), dtype=float) + + +def weekly_amount(people): + adult_people = [p for n, p in people.items() if n != "child"] + couple = len(adult_people) == 2 + disabled = any( + p.get("is_blind") or any(p.get(b, 0) > 0 for b in DISABILITY) + for p in adult_people + ) + carer = any(p.get("is_entitled_to_carer_benefit") for p in adult_people) + lone_parent = "child" in people and not couple + return 20 if (disabled or carer or lone_parent) else (10 if couple else 5) + + +def notional_deductions(person, marriage_allowance): + """CER 1996 reg. 14 on the person's profit, from the inputs: no + generated adult is blind-person's-allowance relieved, born before 1935 + or liable for Class 2 (abolished in 2024-25).""" + profit = max(0, person.get("self_employment_income", 0)) + if profit == 0: + return 0.0 + tax = BASIC_RATE * max(0, profit - PERSONAL_ALLOWANCE - marriage_allowance) + class_4 = 0.0 + if person["age"] < STATE_PENSION_AGE: + class_4 = CLASS_4_MAIN_RATE * min( + max(0, profit - LOWER_PROFITS_LIMIT), + UPPER_PROFITS_LIMIT - LOWER_PROFITS_LIMIT, + ) + return tax + class_4 + + +@SETTINGS +@given(units()) +def test_statute_bounds_differential_and_monotonicity(case): + people = case["people"] + sim = simulate(case) + names = list(people) + adult = np.array([n != "child" for n in names]) + employment = np.array( + [max(0, people[n].get("employment_income", 0)) for n in names], dtype=float + ) + self_employment = np.array( + [max(0, people[n].get("self_employment_income", 0)) for n in names], + dtype=float, + ) + marriage_allowance = person_values(sim, "marriage_allowance") + notional = np.array( + [ + notional_deductions(people[n], marriage_allowance[i]) + for i, n in enumerate(names) + ] + ) + assert np.allclose( + person_values(sim, "pension_credit_self_employment_deductions"), + notional, + atol=0.01, + ) + class_1 = person_values(sim, "ni_class_1_employee") + contributions = 0.5 * person_values(sim, "pension_contributions") + own_net = np.maximum( + 0, employment + self_employment - class_1 - notional - contributions + ) + + income_tax = person_values(sim, "income_tax") + no_employment = person_values( + simulate(without(case, ["employment_income"])), "income_tax" + ) + no_earnings = person_values( + simulate(without(case, ["employment_income", "self_employment_income"])), + "income_tax", + ) + employment_tax = max(0.0, float((income_tax - no_employment)[adult].sum())) + self_employment_tax = max(0.0, float((no_employment - no_earnings)[adult].sum())) + net_earnings = max(0.0, float(own_net[adult].sum()) - employment_tax) + + expected = min(net_earnings, weekly_amount(people) * WEEKS_IN_YEAR) + disregard = unit_value(sim, "pension_credit_earnings_disregard") + assert np.isclose(disregard, expected, atol=0.01) + gross = float((employment + self_employment)[adult].sum()) + assert 0 <= disregard <= min(gross, 20 * WEEKS_IN_YEAR) + 0.01 + + sources = sim.tax_benefit_system.parameters( + f"{YEAR}-06-01" + ).gov.dwp.pension_credit.guarantee_credit.income + total = sum(float(sim.calculate(s, YEAR, map_to="benunit")[0]) for s in sources) + actual_self_employment = ( + self_employment_tax + + float(person_values(sim, "ni_class_2")[adult].sum()) + + float(person_values(sim, "ni_class_4")[adult].sum()) + ) + deductions = ( + float(income_tax[adult].sum()) + + float(person_values(sim, "national_insurance")[adult].sum()) + - actual_self_employment + + float(notional[adult].sum()) + + float(contributions[adult].sum()) + ) + before = max(0.0, total - deductions) + assert np.isclose( + unit_value(sim, "pension_credit_income"), + max(0.0, before - disregard), + atol=0.01, + ) + # Mixed-age couples (a partner under State Pension age) get no guarantee + # credit at all, so the comparison is for units entirely over that age. + if all(p["age"] >= STATE_PENSION_AGE for n, p in people.items() if n != "child"): + gc_without = max(0.0, unit_value(sim, "minimum_guarantee") - before) + assert unit_value(sim, "guarantee_credit") >= gc_without - 0.01 + + +EARNINGS_RISES = st.tuples( + st.sampled_from(["claimant", "partner"]), + st.sampled_from(["employment_income", "self_employment_income"]), + st.sampled_from([1, 50, 260, 1_040, 5_000]), +) + + +def raised(case, rise): + name, source, extra = rise + if name not in case["people"]: + name = "claimant" + people = {n: dict(p) for n, p in case["people"].items()} + people[name][source] = people[name].get(source, 0) + extra + return {"people": people, "country": case["country"]} + + +@SETTINGS +@given(units(), EARNINGS_RISES) +def test_earning_more_never_raises_pension_credit(case, rise): + before = unit_value(simulate(case), "pension_credit_entitlement") + after = unit_value(simulate(raised(case, rise)), "pension_credit_entitlement") + assert after <= before + 0.01 + + +@st.composite +def units_under_the_disregard(draw): + """Units with small earnings and no pension contributions or losses, so + the disregard usually covers the net earnings and no floor on a person's + net earnings binds.""" + case = draw(units()) + for person in case["people"].values(): + person.pop("personal_pension_contributions", None) + if "employment_income" in person: + person["employment_income"] = draw(st.sampled_from([0, 50, 100])) + person["self_employment_income"] = draw(st.sampled_from([0, 0, 120])) + return case + + +@settings(SETTINGS, suppress_health_check=list(HealthCheck)) +@given( + units_under_the_disregard(), + st.tuples( + st.sampled_from(["claimant", "partner"]), + st.sampled_from(["employment_income", "self_employment_income"]), + st.sampled_from([1, 50, 100]), + ), +) +def test_pension_credit_income_is_flat_in_earnings_under_the_disregard(case, rise): + sims = [simulate(case), simulate(raised(case, rise))] + cap = weekly_amount(case["people"]) * WEEKS_IN_YEAR + # Where the disregard covers all the net earnings before and after the + # rise, and Pension Credit income is not floored at nil. + assume( + all( + unit_value(sim, "pension_credit_earnings_disregard") < cap - 0.01 + and unit_value(sim, "pension_credit_income") > 0 + for sim in sims + ) + ) + before, after = (unit_value(sim, "pension_credit_income") for sim in sims) + assert np.isclose(before, after, atol=0.05) + + +def test_benefit_lists_follow_amending_instruments(): + from policyengine_uk.system import system + + def lists(date): + p = system.parameters(date).gov.dwp.pension_credit.earnings_disregard.higher + return set(p.disability_benefits), set(p.disability_benefit_unit_benefits) + + person, unit = lists("2008-10-26") + assert "esa_contrib" not in person and "esa_income" not in unit + person, unit = lists("2008-10-27") + assert "esa_contrib" in person and "esa_income" in unit + person, _ = lists("2013-04-07") + assert "pip" not in person and "armed_forces_independence_payment" not in person + person, _ = lists("2013-04-08") + assert {"pip", "armed_forces_independence_payment"} <= person + for date in ["2008-10-26", "2013-04-08", "2026-04-06"]: + person, _ = lists(date) + assert {"incapacity_benefit", "sda", "attendance_allowance", "dla"} <= person + + +def test_scottish_basic_rate_band_is_twenty_percent_in_every_year(): + from policyengine_uk.system import system + + bands = system.parameters.gov.hmrc.income_tax.rates.scotland.rates.brackets + for year in range(2016, 2030): + assert bands[1].rate(f"{year}-06-01") == 0.2 diff --git a/policyengine_uk/tests/test_pension_credit_income_losses.py b/policyengine_uk/tests/test_pension_credit_income_losses.py index 94e4b5e535..c6d456d4f2 100644 --- a/policyengine_uk/tests/test_pension_credit_income_losses.py +++ b/policyengine_uk/tests/test_pension_credit_income_losses.py @@ -129,8 +129,9 @@ def test_issue_2255_reproducers(): ) # 20,000 less income tax of 20% of (20,000 - 12,570). assert property_loss == (pytest.approx(18_514, abs=1), 0) - # 238 x 52 = 12,376, less wages of 10,000. + # Wages of 10,000 less the 5 pound a week earnings disregard (SPC Regs + # 2002 Sch VI para 5), 260; 238 x 52 = 12,376, less 9,740. assert trading_loss == ( - pytest.approx(10_000, abs=1), - pytest.approx(2_376, abs=1), + pytest.approx(9_740, abs=1), + pytest.approx(2_636, abs=1), ) diff --git a/policyengine_uk/tests/test_pension_credit_late_inputs.py b/policyengine_uk/tests/test_pension_credit_late_inputs.py new file mode 100644 index 0000000000..390c351ee3 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_late_inputs.py @@ -0,0 +1,81 @@ +"""Explicit tax reliefs survive Pension Credit's earnings counterfactuals.""" + +from copy import deepcopy + +import numpy as np +import pytest + +from policyengine_uk import Simulation + + +@pytest.mark.parametrize("late", [False, True]) +def test_explicit_allowances_keep_couple_award_flat(late): + # Two AA recipients qualify for the £20/week combined disregard. + # The explicit £20,000 allowance leaves £148 pension tax after marriage + # allowance; £1,000 wages adds £200 tax and £800 net earnings, all ignored. + # PC income therefore stays £22,000 - £148 = £21,852, and PC £5,986.20. + situation = { + "people": { + "a": { + "age": {"2026": 70}, + "private_pension_income": {"2026": 22_000}, + "attendance_allowance": {"2026": 3_000}, + }, + "b": {"age": {"2026": 70}, "attendance_allowance": {"2026": 3_000}}, + }, + "benunits": {"b": {"members": ["a", "b"], "would_claim_pc": {"2026": True}}}, + "households": {"h": {"members": ["a", "b"]}}, + } + awards = [] + for wages, expected_tax, expected_disregard in [(0, 0, 0), (1_000, 200, 800)]: + data = deepcopy(situation) + data["people"]["a"]["employment_income"] = {"2026": wages} + if not late: + for name, allowance in [("a", 20_000), ("b", 12_570)]: + data["people"][name]["personal_allowance"] = {"2026": allowance} + sim = Simulation(situation=data) + if late: + sim.set_input("personal_allowance", 2026, [20_000, 12_570]) + assert sim.calculate("is_pension_credit_eligible", 2026)[0] + assert sim.calculate("pension_credit_employment_income_tax", 2026)[ + 0 + ] == pytest.approx(expected_tax) + assert sim.calculate("pension_credit_earnings_disregard", 2026)[ + 0 + ] == pytest.approx(expected_disregard) + assert sim.calculate("pension_credit_income", 2026)[0] == pytest.approx(21_852) + awards.append(sim.calculate("pension_credit", 2026)[0]) + np.testing.assert_array_equal( + sim.calculate("personal_allowance", 2026), [20_000, 12_570] + ) + assert sim.branches == {} + assert awards == pytest.approx([5_986.20, 5_986.20], abs=0.01) + + +def test_late_single_person_allowance_survives_counterfactual(): + # £18,000 pension is below the explicit £20,000 allowance. With £10,000 + # wages, £8,000 is taxed at 20%: the entire £1,600 is caused by employment. + sim = Simulation( + situation={ + "people": { + "a": { + "age": {"2026": 75}, + "private_pension_income": {"2026": 18_000}, + "employment_income": {"2026": 10_000}, + } + }, + "benunits": {"b": {"members": ["a"]}}, + "households": {"h": {"members": ["a"], "country": {"2026": "ENGLAND"}}}, + } + ) + sim.set_input("personal_allowance", 2026, [20_000]) + assert sim.calculate("income_tax", 2026)[0] == pytest.approx(1_600) + assert ( + sim.calculate("pension_credit_income_tax_without_employment_income", 2026)[0] + == 0 + ) + assert sim.calculate("pension_credit_employment_income_tax", 2026)[ + 0 + ] == pytest.approx(1_600) + np.testing.assert_array_equal(sim.calculate("personal_allowance", 2026), [20_000]) + assert sim.branches == {} diff --git a/policyengine_uk/tests/test_pension_credit_notional_class2.py b/policyengine_uk/tests/test_pension_credit_notional_class2.py new file mode 100644 index 0000000000..9e177477da --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_notional_class2.py @@ -0,0 +1,116 @@ +"""PC notional Class 2 uses CER chargeable income, before actual NI reliefs. + +CER 1996 reg. 14(2)(a), (3), at 6 April 2023: +https://www.legislation.gov.uk/uksi/1996/2745/regulation/14/2023-04-06 +SSCBA 1992 s. 11(2), (4), at 6 April 2023: +https://www.legislation.gov.uk/ukpga/1992/4/section/11/2023-04-06 +""" + +import pytest + +from policyengine_uk import Simulation + + +@pytest.mark.parametrize( + "profit, expected_deductions", + [(12_570, 0), (13_000, 304.10)], +) +def test_notional_class2_ignores_actual_trading_allowance(profit, expected_deductions): + simulation = Simulation( + situation={ + "people": { + "adult": { + "age": {2023: 60}, + "self_employment_income": {2023: profit}, + "self_employment_gross_receipts": {2023: profit}, + } + } + } + ) + # Known receipts trigger the £1,000 trading allowance for actual NI. + assert simulation.calculate("ni_class_2", 2023)[0] == 0 + # CER chargeable income is profit before that allowance. At £13,000: + # tax = (£13,000 - £12,570) * 20% = £86; + # Class 4 = (£13,000 - £12,570) * 9% = £38.70; + # Class 2 = 52 * £3.45 = £179.40. Total = £304.10. + # At exactly £12,570 CER 14(2)(a) expressly excludes Class 2, and the + # personal allowance/lower profits limit make tax/Class 4 zero too. + assert simulation.calculate("pension_credit_self_employment_deductions", 2023)[ + 0 + ] == pytest.approx(expected_deductions, abs=0.01) + + +def test_other_trade_loss_keeps_protected_couple_notional_class2(): + results = [] + for loss in (0, 20_000): + simulation = Simulation( + situation={ + "people": { + "claimant": { + "age": {2023: 70}, + "attendance_allowance": {2023: 3_000}, + }, + "partner": { + "age": {2023: 60}, + "dla_sc_category": {2023: "MIDDLE"}, + "self_employment_income": {2023: 20_000}, + "trading_loss": {2023: loss}, + }, + }, + "benunits": { + "couple": { + "members": ["claimant", "partner"], + "has_mixed_age_couple_pension_credit_saving": {2023: True}, + "would_claim_pc": {2023: True}, + } + }, + "households": {"home": {"members": ["claimant", "partner"]}}, + } + ) + assert simulation.calculate("is_pension_credit_eligible", 2023)[0] + actual_class2 = simulation.calculate("ni_class_2", 2023)[1] + assert actual_class2 == pytest.approx(0 if loss else 179.40, abs=0.01) + # CER 13(12) prohibits another employment's loss reducing the + # £20,000 positive profit. Notional deductions are therefore + # (£20,000 - £12,570 PA - £1,260 marriage allowance) * 20% = + # £1,234 tax; plus £668.70 Class 4 and £179.40 Class 2 = £2,082.10. + deductions = simulation.calculate( + "pension_credit_self_employment_deductions", 2023 + )[1] + assert deductions == pytest.approx(2_082.10, abs=0.01) + results.append( + ( + deductions, + simulation.calculate("pension_credit_income", 2023)[0], + simulation.calculate("pension_credit", 2023)[0], + ) + ) + # SI 2023/316 art.30(2)(a), (d): the couple guarantee plus the higher + # severe-disability addition is (£306.85 + £152.80) * 52 = £23,901.80. + # https://www.legislation.gov.uk/uksi/2023/316/article/30/made + # PC income = £20,000 - £2,082.10 deductions - £1,040 disregard = + # £16,877.90; the award is £23,901.80 - £16,877.90 = £7,023.90. + assert results[0] == pytest.approx((2_082.10, 16_877.90, 7_023.90), abs=0.01) + assert results[1] == pytest.approx(results[0], abs=0.01) + + +@pytest.mark.parametrize("year, age", [(2023, 70), (2024, 60)]) +def test_notional_class2_respects_pension_age_and_abolition(year, age): + simulation = Simulation( + situation={ + "people": { + "adult": { + "age": {year: age}, + "self_employment_income": {year: 13_000}, + "self_employment_gross_receipts": {year: 13_000}, + } + } + } + ) + # SSCBA s.11(7)(b) excludes pension-age earners, while SI 2024/377 + # reg.8(7) omits CER 14(2)(a) from 6 April 2024. Neither case has a + # notional Class 2 deduction. Class 4 is £25.80 at 2024's 6% main rate. + expected = 86 if year == 2023 else 86 + 25.80 + assert simulation.calculate("pension_credit_self_employment_deductions", year)[ + 0 + ] == pytest.approx(expected, abs=0.01) diff --git a/policyengine_uk/tests/test_pension_credit_tax_branch_reuse.py b/policyengine_uk/tests/test_pension_credit_tax_branch_reuse.py new file mode 100644 index 0000000000..811daf2a9d --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_tax_branch_reuse.py @@ -0,0 +1,95 @@ +"""Share earnings-tax counterfactuals across PC and UC housing calculations.""" + +import numpy as np +import pytest + +from policyengine_uk import Simulation +from policyengine_uk.tests.test_uc_non_dep_pension_credit import YEAR, mixed_families + + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + + +def earnings_families(): + situation = mixed_families() + for name, wages, profit in ( + ("tenant", 18_000, 1_000), + ("pc_older", 1_000, 2_000), + ("pc_younger", 0, 100), + ("working_non_dep", 10_000, 0), + ("renter_older", 1_000, 1_000), + ("renter_younger", 0, 0), + ): + situation["people"][name]["employment_income"] = {YEAR: wages} + situation["people"][name]["self_employment_income"] = {YEAR: profit} + return situation + + +def count_counterfactuals(monkeypatch, sim): + variable = sim.tax_benefit_system.variables[ + "pension_credit_income_tax_without_earnings" + ] + namespace = variable.get_formula(YEAR).__globals__ + original = namespace["income_tax_with_inputs_zeroed"] + calls = [] + + def counted(simulation, period, variables): + calls.append((simulation.branch_name, tuple(variables))) + return original(simulation, period, variables) + + monkeypatch.setitem(namespace, "income_tax_with_inputs_zeroed", counted) + return calls + + +@pytest.mark.parametrize("first", ["pension_credit", "household_net_income"]) +def test_full_calculation_reuses_two_earnings_tax_counterfactuals(monkeypatch, first): + sim = Simulation(situation=earnings_families()) + calls = count_counterfactuals(monkeypatch, sim) + sim.calculate(first, YEAR) + sim.calculate("household_net_income", YEAR) + sim.calculate("pension_credit", YEAR) + # Both employment and self-employment are present. The UC housing + # non-dependant check shares the two counterfactuals with ordinary PC. + assert calls == [ + ("default", ("employment_income",)), + ("default", ("employment_income", "self_employment_income")), + ] + assert sim.branches == {} + + +def test_earnings_tax_reuse_preserves_calculation_order_and_changed_inputs(): + pc_first = Simulation(situation=earnings_families()) + income_first = Simulation(situation=earnings_families()) + pc_first.calculate("pension_credit", YEAR) + income_first.calculate("household_net_income", YEAR) + outputs = ( + "household_net_income", + "pension_credit", + "pension_credit_income", + "pension_credit_employment_income_tax", + "pension_credit_self_employment_income_tax", + "uc_non_dep_receives_pension_credit", + "universal_credit", + ) + for name in outputs: + np.testing.assert_allclose( + pc_first.calculate(name, YEAR), + income_first.calculate(name, YEAR), + atol=0.01, + rtol=0, + ) + wages = [18_000, 50_000, 0, 10_000, 1_000, 0] + pc_first.set_input("employment_income", YEAR, wages) + pc_first.drop_computed_arrays() + changed_situation = earnings_families() + changed_situation["people"]["pc_older"]["employment_income"] = {YEAR: 50_000} + fresh = Simulation(situation=changed_situation) + for name in outputs: + np.testing.assert_allclose( + pc_first.calculate(name, YEAR), + fresh.calculate(name, YEAR), + atol=0.01, + rtol=0, + ) + assert not pc_first.calculate("uc_non_dep_receives_pension_credit", YEAR)[1] + assert pc_first.branches == {} 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 index 95cbe06ea7..5125b98dd2 100644 --- a/policyengine_uk/tests/test_tax_credits_pension_credit_passport_properties.py +++ b/policyengine_uk/tests/test_tax_credits_pension_credit_passport_properties.py @@ -146,9 +146,9 @@ def calculate(sim, year, order=OUTPUTS): 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 is entered at its statutory value (see the -# YAML file). +# hit the third row. Employment income is gross; the model applies SPC Regs +# Sch VI para 5 once. The CTC family element is entered at its statutory +# value (see the YAML file). _PENSIONER = { "age": 70, "employment_income": 2_600, @@ -157,21 +157,27 @@ def calculate(sim, year, order=OUTPUTS): "working_tax_credit_reported": 1, } YAML_FAMILIES = [ - # Guarantee Credit with WTC: passported. + # Gross wages 2,600 - single disregard 5 x 52 = 2,340. With passported + # WTC 2,435, PC is 218.15 x 52 - 6,000 - 2,340 - 2,435 = 568.80. ( [{**_PENSIONER, "state_pension": 6_000}], [], - {"pension_credit_earnings": 2_340}, + {}, {"savings": 10_000}, ), - # Pension Credit only on the income-tested award: the third row. + # The same gross wages leave 2,340 after Sch VI. Passported WTC gives + # PC 11,343.80 - 6,600 - 2,340 - 2,435 < 0, so the passport does not hold; + # income-tested WTC is 2,435 - (9,200 - 7,955) x 41% = 1,924.55, and + # PC remains 11,343.80 - 6,600 - 2,340 - 1,924.55 = 479.25. ( [{**_PENSIONER, "state_pension": 6_600}], [], - {"pension_credit_earnings": 2_340}, + {}, {"savings": 10_000}, ), - # Savings Credit alone, with CTC raised by the passport. + # Gross wages 3,000 - couple disregard 10 x 52 = 2,480; pensions 17,000 + # give PC income 19,480. The 989.976 maximum SC less + # (19,480 - 17,313.40) x 40% gives 123.336, so CTC is passported. ( [ { @@ -185,7 +191,6 @@ def calculate(sim, year, order=OUTPUTS): ], [{"age": 10}], { - "pension_credit_earnings": 2_480, "CTC_family_element": 545, }, {"savings": 10_000}, @@ -202,7 +207,8 @@ def calculate(sim, year, order=OUTPUTS): {"has_mixed_age_couple_pension_credit_saving": False}, {"savings": 10_000}, ), - # The same couple with the saving: Guarantee Credit, passported. + # With the saving, gross wages 2,600 - couple disregard 520 = 2,080. + # PC is 17,313.40 - 6,000 - 2,080 - passported WTC 4,935 = 4,298.40. ( [ {**_PENSIONER, "state_pension": 6_000, "months_since_last_birthday": 9}, @@ -211,7 +217,6 @@ def calculate(sim, year, order=OUTPUTS): [], { "has_mixed_age_couple_pension_credit_saving": True, - "pension_credit_earnings": 2_080, }, {"savings": 10_000}, ), @@ -333,7 +338,9 @@ def test_passport_works_in_traced_and_nested_branch_simulations(): } ], [], - {"pension_credit_earnings": 2_340}, + # Gross wages 2,600 less Sch VI 260 leave 2,340; PC is + # 218.15 x 52 - 6,000 - 2,340 - passported WTC 2,435 = 568.80. + {}, {}, ) for trace in [False, True]: diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py index 85b09326d0..70d7fee070 100644 --- a/policyengine_uk/tests/test_trading_loss_properties.py +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -29,7 +29,9 @@ award equal the same family with those profits cut by the loss. The model's means tests use annual tax deductions, so otherwise the loss reaches them through those deductions. UC's own deductions for tax and - NI on earnings are held alongside Income Tax and NI. + NI on earnings and Pension Credit's tax attribution to employed and + self-employed earnings are held alongside Income Tax and NI, including + the actual Class 2/4 components replaced by notional PC deductions. 5. Tax credits (SI 2002/2006 reg 3(1) Step 4): the claimants' losses (not a child's) come off their applicable income, never below zero, and the award never falls. @@ -610,6 +612,24 @@ def values(sim, variable): @PROPERTY_SETTINGS @example(PINNED_FAMILIES) +@example( + [ + ( + [ + adult(age=18, self_employment_income=17_809.094330166412), + {"age": 0, "trading_loss": 3_692.5341857798658}, + ], + home(1.0, "NORTH_EAST"), + ), + # Actual Class 4 falls to zero with this loss, while CER deductions + # still use the separate positive employment's 30,000 profit. Holding + # total NI also requires holding its actual Class 4 component. + ( + [adult(self_employment_income=30_000, trading_loss=20_000)], + home(0.0), + ), + ] +) @given(st.lists(families(), min_size=4, max_size=10)) def test_means_tests_see_the_loss_only_through_tax(units): with_loss = Simulation(situation=family_situation(units)) @@ -622,10 +642,30 @@ def values(sim, variable): deductions = [ "income_tax", "national_insurance", + # PC replaces these actual components with CER notional deductions; + # hold them consistently with the total national_insurance input. + "ni_class_2", + "ni_class_4", "uc_income_tax_on_earnings", "uc_national_insurance_on_earnings", ] tax = {v: values(with_loss, v) for v in deductions} + # SPC Regs reg 17(11) substitutes CER notional deductions for the tax + # actually attributable to profits. Holding income_tax alone also holds + # it in the no-profit counterfactual, incorrectly setting that attribution + # to zero. Preserve both PC tax allocations from the with-loss side. + # In the child-loss example the adult has no loss: actual/notional tax + # is (17,809.09433 - 12,570) x 20% = 1,047.818866, and notional Class 4 + # is 5,239.09433 x 6% = 314.345660. Holding both tax allocations gives + # PC income 17,809.09433 - 1,047.818866 - 314.345660 - 1,040 = 15,406.93 + # on both sides; the child's loss never enters the claimant's earnings. + pc_tax = { + v: values(with_loss, v) + for v in [ + "pension_credit_employment_income_tax", + "pension_credit_self_employment_income_tax", + ] + } loss = values(with_loss, "trading_loss") profits = values(with_loss, "self_employment_income") mif = np.asarray(with_loss.calculate("uc_mif_applies", YEAR)) @@ -644,7 +684,13 @@ def held(k, **extra): situation=family_situation( units, {name: held(k) for k, name in enumerate(names)}, - {b: {"universal_credit": float(uc[k])} for k, b in enumerate(benunits)}, + { + b: { + "universal_credit": float(uc[k]), + **{v: float(pc_tax[v][k]) for v in pc_tax}, + } + for k, b in enumerate(benunits) + }, ) ) for variable in MEANS_TESTED: diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py index 74fb57f8e4..09bf1fc13f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py @@ -13,19 +13,14 @@ class housing_benefit_savings_credit_only_income(Variable): "authority takes the Secretary of State's assessment of net income for " "that award and modifies it only for the listed items. Modelled: the " "savings credit payable is added, and childcare charges and the " - "Housing Benefit earnings disregards are deducted. Pension Credit " - "income in the model disregards no earnings, whereas the Secretary of " - "State's figure is after the Pension Credit disregards (£20 for a lone " - "parent, £5 single or £10 couple otherwise). Deducting the Housing " - "Benefit disregards (£25 for a lone parent, £5 or £10 otherwise, and " - "the additional £17.10) therefore gives that figure less the higher " - "Housing Benefit amounts the regulation allows: £5 more for a lone " - "parent, and the additional disregard. This holds while neither " - "programme's £20 disregards for disabled people, carers and some " - "occupations are modelled; for those earners the Secretary of State's " - "figure is after £20, so the omission changes the income assessed, the " - "savings credit payable and possibly whether the award is savings " - "credit only. Childcare charges are deducted as in the general " + "higher Housing Benefit earnings disregards are deducted: a lone " + "parent's 25 pounds over the amount disregarded for Pension Credit, " + "and the additional 17.10 pounds. The Secretary of State's figure is " + "Pension Credit income, which is already after the Pension Credit " + "earnings disregards (20 pounds for a lone parent, carer, disabled " + "or blind earner, otherwise 5 pounds single or 10 pounds couple), so " + "Housing Benefit's own 5 and 10 pound amounts are not deducted again. " + "Childcare charges are deducted as in the general " "route, without the work condition or the cap at earnings. Housing " "Benefit's own income and tariff income rules do not apply to the " "Secretary of State's figure, which already counts Pension Credit " @@ -63,22 +58,35 @@ def formula(benunit, period, parameters): childcare = benunit( "housing_benefit_applicable_income_childcare_element", period ) - # Reg 27(4)(c)(i) (Sch 4 para 2: £25 for a lone parent, against SPC - # Regs 2002 Sch VI para 1's £20) and (d) (Sch 4 para 9(1), the - # additional £17.10; the para 5A exempt work disregard is not - # modelled). NI: reg 25(4)(c)(i) and (d), Sch 5 paras 2 and 9(1). - # The model's Pension Credit income applies no Sch VI disregard, so - # deducting the whole Housing Benefit disregard also takes off the Sch - # VI amounts already inside the Secretary of State's figure: £20 for a - # lone parent (para 1) and £5 or £10 otherwise (para 5), which match - # Sch 4 paras 2 and 7 but for the lone parent's extra £5. Neither - # schedule's £20 disregards (Sch VI paras 2 to 4; Sch 4 paras 3 to 5) - # are modelled. If Sch 4 paras 3 to 5 are added to the disregard - # variable, this route needs the Sch VI grounds instead, as reg 27(4) - # does not let Housing Benefit's own £20 grounds replace them. - # Reg 27(5): Housing Benefit's own income rules, including tariff - # income from capital, do not apply to the Secretary of State's figure. - disregard = benunit("housing_benefit_applicable_income_disregard", period) + # Reg 27(4)(c)(i) and (d) (NI: reg 25(4)(c)(i) and (d)): the + # Secretary of State's figure is after the SPC Regs 2002 Sch VI + # earnings disregards (pension_credit_earnings_disregard), so only the + # higher amounts Housing Benefit allows come off: a lone parent's + # Sch 4 para 2 disregard (25 pounds) over the amount disregarded for + # Pension Credit, and the additional disregard of Sch 4 para 9(1) + # (the para 5A exempt work disregard is not modelled). Housing + # Benefit's other grounds do not replace the Pension Credit ones. This + # route applies only under the pension-age Regulations, where the + # disregard is the standard amount (Sch 4 paras 2 and 7) and the + # additional amount (para 9), with no specified-accommodation amount. + p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard + net_earnings = benunit("housing_benefit_net_earnings", period) + lone_parent = benunit("is_lone_parent", period) + weekly_amount = select( + [lone_parent, benunit("is_couple", period)], + [p.lone_parent, p.couple], + default=p.single, + ) + standard = min_(weekly_amount * WEEKS_IN_YEAR, net_earnings) + additional = max_( + 0, benunit("housing_benefit_applicable_income_disregard", period) - standard + ) + higher_lone_parent = where( + lone_parent, + max_(0, standard - benunit("pension_credit_earnings_disregard", period)), + 0, + ) + disregard = higher_lone_parent + additional return max_( 0, pension_credit_income + savings_credit_payable - childcare - disregard, diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py new file mode 100644 index 0000000000..3bbed54e3b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py @@ -0,0 +1,121 @@ +from policyengine_uk.model_api import * + + +class pension_credit_earnings_disregard(Variable): + label = "Pension Credit earnings disregard" + documentation = ( + "Earnings disregarded from Pension Credit income under Schedule VI to " + "the State Pension Credit Regulations 2002: 20 pounds a week for a " + "lone parent (para. 1), where the claimant or partner is a carer " + "satisfying Sch. I para. 4 (para. 3), or where the claimant or partner " + "receives a listed disability benefit (including working tax credit " + "with its disability or severe disability element) or is certified " + "blind (para. 4(1)); otherwise 5 pounds a week for a single claimant and 10 " + "pounds for a couple (para. 5). 20 pounds is the most disregarded " + "however many conditions are met (para. 4A). Reg. 17(9) disregards the " + "sums 'in calculating the claimant's earnings', which are net of income " + "tax and National Insurance (reg. 17(10)) and of half of pension " + "contributions (reg. 17A(4A)), so the disregard never exceeds the " + "claimant's and partner's net earnings, each source floored at nil. " + "Employed earnings (regs. 17(10) and 17A) are net of primary Class 1 " + "contributions and of the income tax payable on them " + "(pension_credit_employment_income_tax). Self-employed earnings (regs. " + "17(11) and 17B) are net of the notional tax and contributions of the " + "Computation of Earnings Regulations 1996 reg. 14 " + "(pension_credit_self_employment_deductions). Half of pension " + "contributions come off both. Not modelled: the employment-specific " + "20 pound " + "disregards of paras. 2 to 2B (part-time firefighters, auxiliary " + "coastguards, lifeboat crew, reserve forces) and the transitional " + "protections of para. 4(2) to (4), and the continued carer " + "qualification of Sch. I para. 4 after carer's allowance stops (the " + "model has no record of when it stopped). The carer condition reads " + "is_entitled_to_carer_benefit, the carer addition's own test, so the " + "two always agree; the Carer's Allowance earnings limit is applied to " + "that test by PolicyEngine/policyengine-uk#2312." + ) + entity = BenUnit + definition_period = YEAR + value_type = float + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17A", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17B", + "https://www.legislation.gov.uk/uksi/1996/2745/regulation/13", + "https://www.legislation.gov.uk/uksi/1996/2745/regulation/14", + ) + + def formula(benunit, period, parameters): + pc = parameters(period).gov.dwp.pension_credit + p = pc.earnings_disregard + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + + on_disability_benefit = ( + add(person, period, p.higher.disability_benefits) > 0 + ) | person("is_blind", period) + unit_disability_benefits = p.higher.disability_benefit_unit_benefits + unit_on_disability_benefit = ( + add(benunit, period, unit_disability_benefits) > 0 + if len(unit_disability_benefits) > 0 + else False + ) + # Para. 4(1)(a)(vi): in receipt of working tax credit that includes + # its disability or severe disability element. + wtc_disability_element = ( + add( + benunit, + period, + ["WTC_disabled_element", "WTC_severely_disabled_element"], + ) + > 0 + ) & (benunit("working_tax_credit", period) > 0) + disabled = ( + benunit.any(claimant_or_partner & on_disability_benefit) + | unit_on_disability_benefit + | wtc_disability_element + ) + # Sch. I para. 4: a claimant or partner entitled to carer's allowance + # (the carer addition's own test), not caring hours alone. + carer = benunit.any( + claimant_or_partner & person("is_entitled_to_carer_benefit", period) + ) + lone_parent = benunit("is_lone_parent", period) + relation_type = benunit("relation_type", period) + weekly = where( + lone_parent | carer | disabled, + p.higher.amount, + p.standard[relation_type], + ) + + # Net earnings of the claimant and partner, each source floored at + # nil: a loss in one employment is not set against another (CER 1996 + # reg. 13(12), applied by SPC Regs 2002 reg. 17B). + # Employed earners (regs. 17(10) and 17A): less the income tax payable + # on the earnings and the primary Class 1 contributions deducted from + # them. Self-employed earners (regs. 17(11) and 17B): less the + # notional tax and contributions of CER 1996 regs. 13(4) and 14. Both: + # less half of pension contributions (reg. 17A(4A); CER 1996 reg. + # 13(4)(c)). + employment = max_(0, person("employment_income", period)) + self_employment = max_(0, person("self_employment_income", period)) + deductions = add( + person, + period, + ["ni_class_1_employee", "pension_credit_self_employment_deductions"], + ) + ( + person("pension_contributions", period) + * pc.income.pension_contributions_deduction + ) + # Each person's deductions come off their own earnings; the income tax + # on employed earnings is measured for the claimant and partner + # together (pension_credit_employment_income_tax). + own_net = max_(0, employment + self_employment - deductions) + net_earnings = max_( + 0, + benunit.sum(own_net * claimant_or_partner) + - benunit("pension_credit_employment_income_tax", period), + ) + return min_(weekly * WEEKS_IN_YEAR, net_earnings) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py new file mode 100644 index 0000000000..cb30beb1d6 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py @@ -0,0 +1,127 @@ +from policyengine_uk.model_api import * + +DECISION = ( + "PolicyEngine does not model PAYE codes, so which part of a person's " + "income tax is payable on their earnings is a methodology choice, put to " + "Max on PolicyEngine/policyengine-uk#2019 and settled as option (a): treat " + "earnings as the top slice of the person's income. " +) + + +def income_tax_with_inputs_zeroed(simulation, period, variables): + """Each person's income tax with every person's value of each of + ``variables`` set to nil, on a branch that is dropped afterwards.""" + name = "pension_credit_without_" + "_and_".join(variables) + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + # Preserve every explicit stored input, including formula overrides + # supplied through set_input after the simulation was constructed. + branch.drop_computed_arrays() + people = branch.populations["person"] + for variable in variables: + branch.set_input(variable, period, np.zeros(people.count)) + return people("income_tax", period) + finally: + del simulation.branches[name] + + +class pension_credit_income_tax_without_employment_income(Variable): + value_type = float + entity = Person + label = "Income tax without employment income, for Pension Credit" + documentation = ( + "The person's income tax if nobody in the simulation had employment " + "income. " + DECISION + ) + definition_period = YEAR + unit = GBP + + def formula(person, period, parameters): + income_tax = person("income_tax", period) + if not (person("employment_income", period) != 0).any(): + return income_tax + return income_tax_with_inputs_zeroed( + person.simulation, period, ["employment_income"] + ) + + +class pension_credit_income_tax_without_earnings(Variable): + value_type = float + entity = Person + label = "Income tax without earnings, for Pension Credit" + documentation = ( + "The person's income tax if nobody in the simulation had employment " + "or self-employment income. " + DECISION + ) + definition_period = YEAR + unit = GBP + + def formula(person, period, parameters): + without_employment = person( + "pension_credit_income_tax_without_employment_income", period + ) + if not (person("self_employment_income", period) != 0).any(): + return without_employment + return income_tax_with_inputs_zeroed( + person.simulation, + period, + ["employment_income", "self_employment_income"], + ) + + +class pension_credit_employment_income_tax(Variable): + value_type = float + entity = BenUnit + label = "Income tax on employed earnings, for Pension Credit" + documentation = ( + "The income tax payable on the claimant's and partner's employed " + "earnings, which SPC Regs 2002 reg. 17(10) disregards in their net " + "amount: the fall in the claimant's and partner's income tax without " + "anyone's employment income, never negative. It includes any tax the " + "earnings add on savings and dividends (a lost starting-rate band or " + "personal savings allowance) and any effect of one partner's earnings " + "on the other's tax (through the marriage allowance), so Pension " + "Credit income does not change with either partner's earnings while " + "the disregard covers them. " + DECISION + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17A", + ) + + def formula(benunit, period, parameters): + person = benunit.members + added = person("income_tax", period) - person( + "pension_credit_income_tax_without_employment_income", period + ) + claimant_or_partner = person("is_claimant_or_partner", period) + return max_(0, benunit.sum(added * claimant_or_partner)) + + +class pension_credit_self_employment_income_tax(Variable): + value_type = float + entity = BenUnit + label = "Income tax actually payable on self-employed profit, for Pension Credit" + documentation = ( + "The income tax the claimant's and partner's self-employment income " + "adds to their income tax, beneath any employment income: the fall in " + "their income tax without anyone's employment income when nobody has " + "self-employment income either, never negative. Pension Credit income " + "replaces it with the notional tax of the Computation of Earnings " + "Regulations 1996 reg. 14 (SPC Regs 2002 reg. 17(11)). " + DECISION + ) + definition_period = YEAR + unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17" + + def formula(benunit, period, parameters): + person = benunit.members + added = person( + "pension_credit_income_tax_without_employment_income", period + ) - person("pension_credit_income_tax_without_earnings", period) + claimant_or_partner = person("is_claimant_or_partner", period) + return max_(0, benunit.sum(added * claimant_or_partner)) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index 944419416e..86e9504e9c 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -10,6 +10,7 @@ class pension_credit_income(Variable): reference = ( "https://www.legislation.gov.uk/ukpga/2002/16/section/5", "https://www.legislation.gov.uk/ukpga/2002/16/section/15", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", ) documentation = ( "Income and deductions of the claimant and partner, together with " @@ -46,10 +47,26 @@ def formula(benunit, period, parameters): pension_contributions = benunit.sum( person("pension_contributions", period) * is_claimant_or_partner ) + # Self-employed earnings are net of the notional tax and contributions + # of the Computation of Earnings Regulations 1996 reg. 14, in place of + # those actually payable on the profit (reg. 17(11)). tax = benunit.sum( - add(person, period, ["income_tax", "national_insurance"]) + ( + add( + person, + period, + [ + "income_tax", + "national_insurance", + "pension_credit_self_employment_deductions", + ], + ) + - add(person, period, ["ni_class_2", "ni_class_4"]) + ) * is_claimant_or_partner - ) + ) - benunit("pension_credit_self_employment_income_tax", period) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate - return max_(0, total - deductions) + # Schedule VI earnings disregards (reg. 17(9)). + earnings_disregard = benunit("pension_credit_earnings_disregard", period) + return max_(0, total - deductions - earnings_disregard) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py new file mode 100644 index 0000000000..d8be62b50a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py @@ -0,0 +1,81 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.class_2 import class_2_contribution_weeks, class_2_liable + + +class pension_credit_self_employment_deductions(Variable): + value_type = float + entity = Person + label = "Notional tax and contributions on self-employed profit, for Pension Credit" + documentation = ( + "The income tax and social security contributions deducted from " + "self-employed earnings for Pension Credit (SPC Regs 2002 regs. 17(11) " + "and 17B; Computation of Earnings Regulations 1996 regs. 13(4) and " + "14), in place of those actually payable on the profit: a notional " + "income tax at the basic rate (the Scottish basic rate for a Scottish " + "taxpayer) on the profit less only the personal reliefs of ITA 2007 " + "Part 3 Chapters 2, 3 and 3A appropriate to their circumstances; " + "Class 4 contributions at the main rate on the profit between the " + "lower and upper profits limits, where Class 4 is payable at all; and " + "the Class 2 contributions payable, until reg. 14(2)(a) was omitted on " + "6 April 2024." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17B", + "https://www.legislation.gov.uk/uksi/1996/2745/regulation/13", + "https://www.legislation.gov.uk/uksi/1996/2745/regulation/14", + "https://www.legislation.gov.uk/uksi/1996/2745/regulation/14/2023-04-06", + "https://www.legislation.gov.uk/ukpga/1992/4/section/11/2023-04-06", + ) + + def formula(person, period, parameters): + profit = max_(0, person("self_employment_income", period)) + rates = parameters(period).gov.hmrc.income_tax.rates + # The Scottish basic rate is the band after the starter rate; read the + # band itself, since the scale at an instant leaves out the starter + # band before 2018-19. + scottish_bands = parameters.gov.hmrc.income_tax.rates.scotland.rates + scottish_basic_rate = scottish_bands.brackets[1].rate(period.start) + basic_rate = where( + person("pays_scottish_income_tax", period), + scottish_basic_rate, + rates.uk.rates[0], + ) + # Chapter 2 (personal and blind person's allowances) and Chapter 3A + # (marriage allowance, which the model deducts from income) relieve + # income; Chapter 3 (married couple's allowance) reduces the tax. + reliefs = add( + person, + period, + ["personal_allowance", "blind_persons_allowance", "marriage_allowance"], + ) + notional_tax = max_( + 0, + basic_rate * max_(0, profit - reliefs) + - person("married_couples_allowance_deduction", period), + ) + class_4 = parameters(period).gov.hmrc.national_insurance.class_4 + lower = class_4.thresholds.lower_profits_limit + upper = class_4.thresholds.upper_profits_limit + notional_class_4 = where( + person("ni_class_4_liable", period), + class_4.rates.main * min_(max_(0, profit - lower), upper - lower), + 0, + ) + class_2 = parameters(period).gov.hmrc.national_insurance.class_2 + # CER 14(2)(a), (3), before 6 April 2024: test this employment's + # chargeable income, not actual NI profits after trading allowance + # or other employment losses. SSCBA s.11 supplies the rate and + # age-liability rules. The compulsory rate is nil from 2024-25. + notional_class_2 = ( + person("ni_liable", period) + * class_2_liable(profit, class_2) + * class_2.flat_rate + * class_2_contribution_weeks(period.start.year) + ) + return where( + profit > 0, + notional_tax + notional_class_4 + notional_class_2, + 0, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py index 483b5c7cd1..84dd97e376 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py @@ -30,6 +30,10 @@ def formula(benunit, period, parameters): # their actual Pension Credit calculation while breaking the cycle # through the renter's UC entitlement and pension-age route. simulation = benunit.simulation + # Income tax is independent of the UC housing element. Calculate + # both earnings counterfactuals on the parent so this branch and + # ordinary Pension Credit share them whichever is requested first. + benunit.members("pension_credit_income_tax_without_earnings", period) name = "uc_non_dep_pension_credit" while name in simulation.branches or name == simulation.branch_name: name += "_"