diff --git a/changelog.d/2090.fixed.md b/changelog.d/2090.fixed.md new file mode 100644 index 0000000000..fd65a3c255 --- /dev/null +++ b/changelog.d/2090.fixed.md @@ -0,0 +1 @@ +The model now deducts from Council Tax Reduction income only the income tax and National Insurance each scheme allows: in Scotland's working-age scheme the extra tax earnings cause and the notional tax on self-employed profit, and in the pensioner schemes of England, Wales and Scotland that same earnings tax, the tax on the other counted income and the notional tax on profit, with no tax on savings, dividends, rent from property or the Scottish Carer Supplement, and no High Income Child Benefit Charge, where earnings do not raise them. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_carer_support_payment.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_carer_support_payment.yaml index f18404306e..37cc2e7206 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_carer_support_payment.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_carer_support_payment.yaml @@ -10,11 +10,14 @@ # Neither counts the Scottish Carer Supplement, paid with CSP from 15 March # 2026: reg 57(1) does not list it and reg 27(1)(j)(xxib) excepts it. In law # the supplement's tax does not reach Council Tax Reduction either: at working -# age SSI 2021/249 deducts tax only from earnings, and at pension age SSI -# 2012/319 reg 31(12) disregards the tax payable on income taken into -# account, which the supplement is not. The model deducts all income tax, so -# the taxable supplement reaches the model's Council Tax Reduction income -# through income tax and in no other way. +# age the Scottish Ministers let the council deduct tax only from earnings (SSI +# 2021/249), and at pension age the council disregards the tax payable on +# income taken into account (SSI 2012/319 reg 31(12)), which the supplement is +# not. The model deducts only the tax each scheme allows +# (council_tax_reduction_tax_deduction). At working age that is the tax +# earnings add, and at pension age the tax on the counted income with the +# supplement above it, so without earnings the supplement does not reach the +# model's Council Tax Reduction income. # Neither instrument disregards CSP above the CA amount. # # Weekly rates. 2026: CSP and CA 86.45, Scottish Carer Supplement 11.70, @@ -248,7 +251,7 @@ # jumps to 2,000 when the carer moves to Carer Support Payment in 2025. council_tax_reduction: 2_000 - 0.2 * (7_000 + 81.90 * 52 - (90.50 + 45.60) * 52) -- name: Pension-age Council Tax Reduction counts Carer Support Payment, and the supplement only through tax +- name: Pension-age Council Tax Reduction counts Carer Support Payment and deducts no tax on the supplement period: 2026 absolute_error_margin: 0.01 input: @@ -275,13 +278,19 @@ # first 3,967 at the Scottish starter rate of 19% and 2,566.80 at 20%. # The supplement adds 608.40 x 20% = 121.68 of that tax. income_tax: 3_967 * 0.19 + 2_566.80 * 0.20 - # 14,000 + 4,495.40 - 1,267.09: the supplement is not added. - council_tax_reduction_applicable_income: 14_000 + 86.45 * 52 - 1_267.09 + # Under SSI 2012/319 reg 31(12), the council disregards only the tax on + # income taken into account. The supplement is not, so the formula leaves + # out its tax, as the highest part of income: 14,000 + 4,495.40 - 12,570 = + # 5,925.40 taxable, 3,967 x 19% = 753.73 and 1,958.40 x 20% = 391.68, so + # 1,145.41. + council_tax_reduction_tax_deduction: 3_967 * 0.19 + 1_958.40 * 0.20 + # 14,000 + 4,495.40 - 1,145.41: neither the supplement nor its tax counts. + council_tax_reduction_applicable_income: 14_000 + 86.45 * 52 - 1_145.41 council_tax_reduction_applicable_amount: (256 + 48.15) * 52 # Income is above the minimum guarantee of (238 + 48.15) x 52 = 14,879.80. guarantee_credit: 0 - # 2,000 - 0.2 x (17,228.31 - 15,815.80) - council_tax_reduction: 1_717.50 + # 2,000 - 0.2 x (17,349.99 - 15,815.80) + council_tax_reduction: 1_693.16 - name: A pension-age couple's Council Tax Reduction counts the partner's Carer Support Payment period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.yaml new file mode 100644 index 0000000000..2e118cd2ea --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.yaml @@ -0,0 +1,920 @@ +# Income tax and National Insurance deducted from Council Tax Reduction income. +# - Scotland, working age (SSI 2021/249): in reg 50(3)(a), the Scottish +# Ministers provide that "Earnings do not include— (a) any amount deducted +# from them by way of income tax or primary Class 1 contributions under the +# 1992 Act". They set the same rule with Universal Credit (regs 49(6)(b) and +# 51(2) step 3) and gave no tax deduction for unearned income (regs 57 to +# 65). Without Universal Credit, in reg 51(9) they apply SSI 2012/303 regs 37 +# and 38, under which the council prices the tax and contributions on +# self-employed profit notionally: the basic rate on chargeable income less +# only the personal allowance, and Class 4 at the main rate between the +# lower and upper limits. +# - Pension age: in SSI 2012/319 reg 31(12), the Scottish Ministers provide +# that "In the case of any income taken into account for the purpose of +# calculating a person's income, any amount payable by way of tax must be +# disregarded"; the Secretary of State (SI 2012/2885 Sch 1 para 17(13)) and +# the Welsh Ministers (WSI 2013/3029 Sch 1 para 11(13)) make the same rule. +# Under reg 33(2)(a), para 19(2)(a) and para 13(2)(a), the council deducts +# the income tax and Class 1 deducted from earnings, and under reg 37, para +# 30 and para 24 it prices self-employed profit notionally, as at working +# age. +# No provision says how to split one income tax liability. The model follows +# the choice settled for Pension Credit on PolicyEngine/policyengine-uk#2019 +# (option (a)): the tax on earnings is the extra tax the earnings cause, with +# the earnings as the top slice of income and self-employed profit beneath +# them. In the pensioner schemes the tax on the other counted income is the +# tax it bears with the uncounted income above it. +# +# 2026-27 rates. Personal allowance 12,570. Scottish bands on taxable non- +# savings income: 19% to 3,967, 20% to 16,956, 21% to 31,092, 42% to 62,430. +# UK basic rate 20% to 37,700, higher rate 40%. Savings: starting rate band +# 5,000 less taxable non-savings income; personal savings allowance 1,000 for +# a basic-rate taxpayer and 500 for a higher-rate one. Class 1 primary +# contributions 8% above 241.73 a week (12,569.96 a year). Class 4 6% from +# 12,570 to 50,270. Each family is a single adult with no Pension Credit or +# Universal Credit unless stated. + +- name: Scotland's working-age scheme deducts the tax that pay adds to a pension + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 20_000 + private_pension_income: 10_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # Tax with the pay: 30,000 - 12,570 = 17,430 taxable; 3,967 x 19% = 753.73, + # 12,989 x 20% = 2,597.80 and 474 x 21% = 99.54, so 3,451.07. Tax without + # the pay: 10,000 is below the personal allowance, so nil. The pay adds + # 3,451.07. Class 1: (20,000 - 12,569.96) x 8% = 594.40. + council_tax_reduction_tax_deduction: 4_045.47 + # 20,000 + 10,000 - 4,045.47. + council_tax_reduction_applicable_income: 25_954.53 + +- name: Scotland's working-age scheme deducts the savings tax that pay adds + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 30_000 + savings_interest_income: 60_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # With the pay: non-savings 17,430 taxable, 3,451.07 as above. Savings + # 60,000 sit above 17,430 in the UK bands: no starting rate; 500 personal + # savings allowance; 37,700 - 17,430 - 500 = 19,770 x 20% = 3,954; and + # 60,000 - 20,270 = 39,730 x 40% = 15,892. Total 23,297.07. + # Without the pay: 60,000 - 12,570 = 47,430 taxable savings; 5,000 at the + # starting rate and 500 allowance at nil, 32,200 x 20% = 6,440 and + # 9,730 x 40% = 3,892, so 10,332. The pay adds 12,965.07. + # Class 1: (30,000 - 12,569.96) x 8% = 1,394.40. + council_tax_reduction_tax_deduction: 14_359.47 + # The scheme counts the pay alone: 30,000 - 14,359.47. + council_tax_reduction_applicable_income: 15_640.53 + +- name: Scotland's working-age scheme deducts the tax that pay adds to property income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 30_000 + property_income: 20_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # With the pay: 50,000 - 12,570 = 37,430 taxable; 753.73 + 2,597.80 + + # 14,136 x 21% = 2,968.56 + 6,338 x 42% = 2,661.96, so 8,982.05. Without + # the pay: 7,430 taxable; 753.73 + 3,463 x 20% = 692.60, so 1,446.33. The + # pay adds 7,535.72. Class 1 1,394.40. + council_tax_reduction_tax_deduction: 8_930.12 + # The scheme counts the pay but not the rent from property, which the + # model treats as income from capital: 30,000 - 8,930.12. + council_tax_reduction_applicable_income: 21_069.88 + +- name: Scotland's working-age scheme keeps benefits in kind beneath the pay + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 30_000 + company_car_benefit: 15_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # Reg 50(3)(e): a payment in kind is not earnings, so the car benefit stays + # in both terms. With the pay: 45,000 - 12,570 = 32,430 taxable; 753.73 + + # 2,597.80 + 2,968.56 + 1,338 x 42% = 561.96, so 6,882.05. Without the pay: + # 15,000 - 12,570 = 2,430 x 19% = 461.70. The pay adds 6,420.35. Class 1 + # 1,394.40. + council_tax_reduction_tax_deduction: 7_814.75 + # 30,000 - 7,814.75: the car benefit is not counted. + council_tax_reduction_applicable_income: 22_185.25 + +- name: Scotland's working-age scheme prices tax on a 15,000 profit notionally + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + self_employment_income: 15_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # SSI 2012/303 reg 38: (15,000 - 12,570) x 20% = 486 and Class 4 + # (15,000 - 12,570) x 6% = 145.80. That exceeds the 461.70 + 145.80 the + # person pays. + council_tax_reduction_tax_deduction: 631.80 + # 15,000 - 631.80. + council_tax_reduction_applicable_income: 14_368.20 + +- name: Scotland's working-age scheme prices tax on a 60,000 profit notionally + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + self_employment_income: 60_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # (60,000 - 12,570) x 20% = 9,486, and Class 4 only between the limits: + # (50,270 - 12,570) x 6% = 2,262. + council_tax_reduction_tax_deduction: 11_748 + # 60,000 - 11,748. + council_tax_reduction_applicable_income: 48_252 + +- name: Scotland's working-age scheme with Universal Credit deducts the tax that profit adds + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + self_employment_income: 15_000 + savings_interest_income: 20_000 + child_1: + age: 5 + child_2: + age: 3 + benunits: + benunit: + members: [parent, child_1, child_2] + would_claim_uc: true + households: + household: + members: [parent, child_1, child_2] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + universal_credit: 7_190.20 + # Reg 51(2) step 3 deducts the tax and contributions paid. With the + # profit: non-savings 2,430 x 19% = 461.70; savings 20,000 over a 2,570 + # starting rate band and a 1,000 allowance, 16,430 x 20% = 3,286; total + # 3,747.70. Without it: 7,430 taxable savings; 5,000 starting rate, 1,000 + # allowance, 1,430 x 20% = 286. The profit adds 3,461.70. Class 4 + # charged: 145.80. + council_tax_reduction_tax_deduction: 3_607.50 + # 15,000 - 3,607.50, plus Universal Credit 7,190.20 and Child Benefit + # (27.05 + 17.90) x 52 = 2,337.40. The interest is not counted. + council_tax_reduction_applicable_income: 20_920.10 + +- name: Scotland's working-age scheme counted Class 2 in the notional deduction before 2024-25 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + self_employment_income: 20_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # 2023-24: SSI 2012/303 reg 38 as it stood on 5 April 2024. Income tax + # (20,000 - 12,570) x 20% = 1,486; Class 4 (20,000 - 12,570) x 9% = + # 668.70; Class 2, as the profit exceeds the 12,570 lower profits + # threshold, 3.45 x 52 weeks = 179.40. + council_tax_reduction_tax_deduction: 2_334.10 + # 20,000 - 2,334.10. + council_tax_reduction_applicable_income: 17_665.90 + +- name: Scotland's working-age scheme deducts no voluntary Class 3 contributions + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 20_000 + ni_class_3: 900 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # Tax on the pay: 7,430 taxable; 753.73 + 692.60 = 1,446.33. Class 1: + # 594.40. Class 3 is voluntary, not a primary Class 1 contribution + # deducted from earnings, so the 900 does not come off. + council_tax_reduction_tax_deduction: 2_040.73 + # 20,000 - 2,040.73. + council_tax_reduction_applicable_income: 17_959.27 + +- name: England's pensioner scheme deducts no tax on savings interest + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 20_308 + savings_interest_income: 6_559 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # The scheme counts the pension, not the interest. Tax on the pension + # with the interest above it: (20,308 - 12,570) x 20% = 1,547.60. + council_tax_reduction_tax_deduction: 1_547.60 + # 20,308 - 1,547.60. + council_tax_reduction_applicable_income: 18_760.40 + +- name: England's pensioner scheme deducts no tax on rent from a property the pensioner doesn't live in + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 20_000 + property_income: 10_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # The pensioner lets out a flat they don't live in, for a profit of + # 10,000. The scheme counts the pension, not the rent, which the model + # treats as income from capital; the case gives the flat no value, so the + # model counts no capital and no tariff income. Income tax: 30,000 - + # 12,570 = 17,430 x 20% = 3,486. Tax on the pension with the rent above + # it: (20,000 - 12,570) x 20% = 1,486. + income_tax_before_winter_fuel_payment_charge: 3_486 + council_tax_reduction_tax_deduction: 1_486 + # 20,000 - 1,486. + council_tax_reduction_applicable_income: 18_514 + # A single pensioner who reached State Pension age after 1 April 2021 + # gets 238 a week, 12,376 a year: 2,000 - 20% x (18,514 - 12,376) = + # 772.40. + council_tax_reduction_applicable_amount: 12_376 + council_tax_reduction: 772.40 + +- name: England's pensioner scheme deducts the savings tax that pay adds + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + employment_income: 20_000 + savings_interest_income: 20_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # With the pay: 7,430 x 20% = 1,486 on the pay; savings 20,000 with no + # starting rate, a 1,000 allowance and 19,000 x 20% = 3,800; total 5,286. + # Without the pay: 7,430 taxable savings; 5,000 starting rate, 1,000 + # allowance, 1,430 x 20% = 286. The pay adds 5,000. The scheme counts no + # other taxed income, and the person pays no Class 1 above pension age. + council_tax_reduction_tax_deduction: 5_000 + # 20,000 - 5,000 - the 5 a week earnings disregard (260). + council_tax_reduction_applicable_income: 14_740 + +- name: England's pensioner scheme prices tax on a profit notionally + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + self_employment_income: 60_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # SI 2012/2885 Sch 1 para 30: (60,000 - 12,570) x 20% = 9,486, not the + # 11,432 the profit bears. No Class 4 is payable above pension age. + council_tax_reduction_tax_deduction: 9_486 + # 60,000 - 9,486 - 260. + council_tax_reduction_applicable_income: 50_254 + +- name: Wales's pensioner scheme deducts no tax on dividends + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 20_000 + dividend_income: 10_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: WALES + region: WALES + council_tax: 2_000 + output: + # The scheme counts the pension, not the dividends. Tax on the pension: + # (20,000 - 12,570) x 20% = 1,486. + council_tax_reduction_tax_deduction: 1_486 + # 20,000 - 1,486. + council_tax_reduction_applicable_income: 18_514 + +- name: Wales's pensioner scheme prices tax on a profit notionally + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + self_employment_income: 60_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: WALES + region: WALES + council_tax: 2_000 + output: + # WSI 2013/3029 Sch 1 para 24: (60,000 - 12,570) x 20% = 9,486, not the + # 11,432 the profit bears. No Class 4 is payable above pension age. + council_tax_reduction_tax_deduction: 9_486 + # 60,000 - 9,486 - 260. + council_tax_reduction_applicable_income: 50_254 + +- name: Scotland's pensioner scheme deducts no tax on the Scottish Carer Supplement + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 15_000 + # 11.70 a week for 52 weeks. + scottish_carer_supplement: 608.40 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # The Scottish pensioner scheme does not count the Scottish Carer + # Supplement as income (SSI 2012/319 reg 27(1)(j)(xxib)), so its tax, as + # the highest part of income, does not come off. Tax on the pension: + # (15,000 - 12,570) x 19% = 461.70. + council_tax_reduction_tax_deduction: 461.70 + # 15,000 - 461.70. + council_tax_reduction_applicable_income: 14_538.30 + +- name: Scotland's pensioner scheme prices tax on a profit notionally + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + self_employment_income: 60_000 + benunits: + benunit: + members: [pensioner] + households: + household: + members: [pensioner] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # SSI 2012/319 reg 37: (60,000 - 12,570) x 20% = 9,486, not the 13,182.05 + # in Scottish bands the profit bears. No Class 4 above pension age. + council_tax_reduction_tax_deduction: 9_486 + # 60,000 - 9,486 - 260. + council_tax_reduction_applicable_income: 50_254 + +- name: England's pensioner scheme deducts no High Income Child Benefit Charge + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 70_000 + child: + age: 10 + benunits: + benunit: + members: [pensioner, child] + would_claim_uc: false + would_claim_child_benefit: true + households: + household: + members: [pensioner, child] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # Tax on the pension: 70,000 - 12,570 = 57,430 taxable; 37,700 x 20% = + # 7,540 and 19,730 x 40% = 7,892, so 15,432. Child Benefit 27.05 x 52 = + # 1,406.60, 1,406 in whole pounds; the charge takes (70,000 - 60,000) / + # 200 = 50% of it, 703, so income tax before the winter fuel payment + # charge comes to 16,135. The scheme does not count Child Benefit, and + # through the charge HMRC recovers it, so the formula treats the charge as + # not tax payable on income taken into account. + income_tax_before_winter_fuel_payment_charge: [16_135, 0] + council_tax_reduction_tax_deduction: 15_432 + # 70,000 - 15,432; the scheme does not count Child Benefit. + council_tax_reduction_applicable_income: 54_568 + +- name: England's pensioner scheme deducts the same tax without a Child Benefit claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 70_000 + child: + age: 10 + benunits: + benunit: + members: [pensioner, child] + would_claim_uc: false + would_claim_child_benefit: false + households: + household: + members: [pensioner, child] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # No Child Benefit, so no charge: (70,000 - 12,570) taxed as above, 15,432. + income_tax_before_winter_fuel_payment_charge: [15_432, 0] + council_tax_reduction_tax_deduction: 15_432 + # 70,000 - 15,432. + council_tax_reduction_applicable_income: 54_568 + +- name: England's pensioner scheme deducts the rise in the Child Benefit charge that pay causes + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 70_000 + employment_income: 5_000 + child: + age: 10 + benunits: + benunit: + members: [pensioner, child] + would_claim_uc: false + would_claim_child_benefit: true + households: + household: + members: [pensioner, child] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # With the pay: 75,000 - 12,570 = 62,430 taxable; 7,540 + 24,730 x 40% = + # 9,892, so 17,432; the charge takes (75,000 - 60,000) / 200 = 75% of + # 1,406, 1,054 in whole pounds; T = 18,486. Without the pay: 15,432 and + # a charge of 703, T(E) = 16,135. T - T(E) = 2,351: the 2,000 of tax on + # the pay and the 351 rise in the charge. T(E, S, U) leaves out the charge + # the pensioner would pay without the pay: 15,432. No Class 1 above + # pension age. 2,351 + 15,432 = 17,783. + income_tax_before_winter_fuel_payment_charge: [18_486, 0] + council_tax_reduction_tax_deduction: 17_783 + # 75,000 - 17,783 - the 25 a week lone parent's earnings disregard (1,300). + council_tax_reduction_applicable_income: 55_917 + +- name: England's pensioner scheme deducts a Child Benefit charge that pay causes in full + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 50_000 + employment_income: 30_000 + child: + age: 10 + benunits: + benunit: + members: [pensioner, child] + would_claim_uc: false + would_claim_child_benefit: true + households: + household: + members: [pensioner, child] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # Without the pay: (50,000 - 12,570) x 20% = 7,486, and adjusted net + # income 50,000 brings no charge. With it: 80,000 - 12,570 = 67,430 + # taxable; the pay adds 270 x 20% = 54 and 29,730 x 40% = 11,892, so + # 11,946; the charge takes (80,000 - 60,000) / 200 = 100% of 1,406. T = + # 7,486 + 11,946 + 1,406 = 20,838; T(E) = 7,486; T(E, S, U) = 7,486. The + # deduction comes to the whole liability. + income_tax_before_winter_fuel_payment_charge: [20_838, 0] + council_tax_reduction_tax_deduction: 20_838 + # 80,000 - 20,838 - 1,300. + council_tax_reduction_applicable_income: 57_862 + +- name: England's pensioner scheme deducts no pension annual allowance charge + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 70_000 + personal_pension_contributions: 70_000 + benunits: + benunit: + members: [pensioner] + would_claim_uc: false + households: + household: + members: [pensioner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # The pensioner has no earnings, so the model gives relief on 3,600 of + # the contributions: 70,000 - 3,600 - 12,570 = 53,830 taxable; 37,700 x + # 20% = 7,540 and 16,130 x 40% = 6,452, so 13,992. The contributions + # exceed the 60,000 annual allowance by 10,000, and the model charges that + # amount at the higher rate, 40%, so the charge comes to 4,000 and income + # tax before the winter fuel payment charge to 17,992. + personal_pension_contributions_tax: 4_000 + income_tax_before_winter_fuel_payment_charge: 17_992 + # The charge falls on the contributions, not on income the scheme + # counts, so the formula leaves it out of T(E, S, U): the deduction comes + # to the 13,992 of tax on the pension. + council_tax_reduction_tax_deduction: 13_992 + +- name: England's pensioner scheme deducts a working-age partner's Class 1 contributions + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 20_000 + partner: + age: 60 + employment_income: 20_000 + benunits: + benunit: + members: [pensioner, partner] + would_claim_uc: false + households: + household: + members: [pensioner, partner] + country: ENGLAND + region: NORTH_WEST + council_tax: 2_000 + output: + # Under para 19(2)(a), the council deducts the income tax and primary + # Class 1 deducted from the partner's pay. T: the pension bears (20,000 - + # 12,570) x 20% = 1,486 and the pay the same 1,486, so 2,972. Without the + # pay the partner has no income, and the model transfers 1,260 of + # Marriage Allowance to the pensioner, whose tax falls to (20,000 - + # 12,570 - 1,260) x 20% = 1,234: T(E) = 1,234, so the pay adds 2,972 - + # 1,234 = 1,738. T(E, S, U) = 1,234. Class 1 (20,000 - 12,569.96) x 8% = + # 594.40. 1,738 + 1,234 + 594.40 = 3,566.40. + council_tax_reduction_tax_deduction: 3_566.40 + # 40,000 - 3,566.40 - the 10 a week couple's earnings disregard (520). + council_tax_reduction_applicable_income: 35_913.60 + +- name: Scotland's pensioner scheme allows only the personal relief against a profit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + self_employment_income: 30_000 + younger: + age: 68 + private_pension_income: 10_000 + benunits: + benunit: + members: [older, younger] + households: + household: + members: [older, younger] + country: SCOTLAND + region: SCOTLAND + council_tax: 2_000 + output: + # The younger partner's pension falls within the personal allowance, so + # the model transfers 1,260 of Marriage Allowance to the older partner. + marriage_allowance: [1_260, 0] + # The Scottish Ministers allow "only the personal relief" (SSI 2012/319 + # reg 37(1)), so the Marriage Allowance does not come off: (30,000 - + # 12,570) x 20% = 3,486. No Class 4 above pension age. T(E, S, U): + # without the profit the older partner has no income and the pension + # bears no tax, so nil. No employed earnings. + council_tax_reduction_tax_deduction: 3_486 + # 40,000 - 3,486 - the 10 a week couple's earnings disregard (520). + council_tax_reduction_applicable_income: 35_994 + +- name: Wales's working-age scheme deducts the tax the legacy means tests deduct + period: 2026 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 40 + employment_income: 20_000 + savings_interest_income: 20_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + country: WALES + region: WALES + council_tax: 2_000 + output: + # The formula leaves this scheme as the legacy means tests have it: the + # income tax less the tax the interest would bear without the pay + # (legacy_means_test_deducted_income_tax), and National Insurance. + # With the pay: non-savings 20,000 - 12,570 = 7,430 taxable, x 20% = + # 1,486; the interest above it gets no starting rate, a 1,000 personal + # savings allowance and 19,000 x 20% = 3,800, so 5,286. Without the pay: + # 20,000 - 12,570 = 7,430 of taxable interest, 5,000 at the starting rate + # and 1,000 allowance at nil, 1,430 x 20% = 286. 5,286 - 286 = 5,000. + # Class 1: (20,000 - 12,569.96) x 8% = 594.40. + legacy_means_test_deducted_income_tax: 5_000 + council_tax_reduction_tax_deduction: 5_594.40 + # The scheme counts the pay but not the interest: 20,000 - 5,594.40. + council_tax_reduction_applicable_income: 14_405.60 + +- name: Scotland's working-age scheme passes the Marriage Allowance cliff through at pay of 43,661 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 43_661 + partner: + age: 40 + benunits: + benunit: + members: [earner, partner] + would_claim_uc: false + households: + household: + members: [earner, partner] + country: SCOTLAND + region: SCOTLAND + council_tax: 15_000 + output: + # Taxable non-savings income 43,661 - 12,570 = 31,091, within the + # intermediate band, so the earner takes the partner's 1,260 of Marriage + # Allowance, which the model takes off taxable income: 29,831 taxable; + # 3,967 x 19% = 753.73, 12,989 x 20% = 2,597.80 and 12,875 x 21% = + # 2,703.75, so 6,055.28. Without the pay, nil. Class 1 (43,661 - + # 12,569.96) x 8% = 2,487.28. 6,055.28 + 2,487.28 = 8,542.56. + marriage_allowance: [1_260, 0] + council_tax_reduction_tax_deduction: 8_542.56 + # 43,661 - 8,542.56. + council_tax_reduction_applicable_income: 35_118.44 + # 15,000 - 20% x (35,118.44 - the couple's allowance 150.15 x 52 = + # 7,807.80). + council_tax_reduction: 9_537.87 + +- name: Scotland's working-age scheme passes the Marriage Allowance cliff through at pay of 43,663 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 43_663 + partner: + age: 40 + benunits: + benunit: + members: [earner, partner] + would_claim_uc: false + households: + household: + members: [earner, partner] + country: SCOTLAND + region: SCOTLAND + council_tax: 15_000 + output: + # Taxable 31,093 exceeds the Scottish intermediate limit of 31,092, so + # the earner pays 1 pound at the higher rate and gets no Marriage + # Allowance: 753.73 + 2,597.80 + 14,136 x 21% = 2,968.56 + 1 x 42% = + # 0.42, so 6,320.51. Class 1 (43,663 - 12,569.96) x 8% = 2,487.44. + # 6,320.51 + 2,487.44 = 8,807.95: two pounds of pay raise the deduction + # by 265.39. + marriage_allowance: [0, 0] + council_tax_reduction_tax_deduction: 8_807.95 + # 43,663 - 8,807.95. + council_tax_reduction_applicable_income: 34_855.05 + # 15,000 - 20% x (34,855.05 - 7,807.80): the reduction rises by 52.68. + council_tax_reduction: 9_590.55 + +- name: Scotland's working-age scheme passes a Child Benefit charge step through at pay of 64,999 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 64_999 + child_1: + age: 4 + child_2: + age: 7 + benunits: + benunit: + members: [parent, child_1, child_2] + would_claim_uc: false + would_claim_child_benefit: true + households: + household: + members: [parent, child_1, child_2] + country: SCOTLAND + region: SCOTLAND + council_tax: 15_000 + output: + # Taxable 64,999 - 12,570 = 52,429: 3,967 x 19% = 753.73, 12,989 x 20% = + # 2,597.80, 14,136 x 21% = 2,968.56 and 21,337 x 42% = 8,961.54, so + # 15,281.63. Child Benefit (27.05 + 17.90) x 52 = 2,337.40, 2,337 in whole + # pounds. Adjusted net income exceeds 60,000 by 4,999, or 24 whole steps + # of 200, so the charge takes 24% of 2,337 = 560.88, 560 in whole pounds. + # T = 15,841.63. Without the pay, nil. Class 1: 37,700 x 8% = 3,016 up to + # the upper earnings limit of 966.73 a week (50,269.96 a year), and + # (64,999 - 50,269.96) x 2% = 294.58, so 3,310.58. 15,841.63 + 3,310.58 = + # 19,152.21. + CB_HITC: [560, 0, 0] + council_tax_reduction_tax_deduction: 19_152.21 + # The model counts the pay and the Child Benefit: 64,999 + 2,337.40 - + # 19,152.21. + council_tax_reduction_applicable_income: 48_184.19 + # 15,000 - 20% x (48,184.19 - the lone parent's allowance 95.55 x 52 = + # 4,968.60). + council_tax_reduction: 6_356.88 + +- name: Scotland's working-age scheme passes a Child Benefit charge step through at pay of 65,000 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 65_000 + child_1: + age: 4 + child_2: + age: 7 + benunits: + benunit: + members: [parent, child_1, child_2] + would_claim_uc: false + would_claim_child_benefit: true + households: + household: + members: [parent, child_1, child_2] + country: SCOTLAND + region: SCOTLAND + council_tax: 15_000 + output: + # Taxable 52,430: one more pound at 42%, so 15,282.05. Adjusted net income + # exceeds 60,000 by 5,000, or 25 steps of 200, so the charge takes 25% of + # 2,337 = 584.25, 584 in whole pounds. T = 15,866.05. Class 1 3,016 + + # (65,000 - 50,269.96) x 2% = 294.60, so 3,310.60. 15,866.05 + 3,310.60 = + # 19,176.65: a pound of pay raises the deduction by 24.44, and the charge + # gives 24 of that. + CB_HITC: [584, 0, 0] + council_tax_reduction_tax_deduction: 19_176.65 + # 65,000 + 2,337.40 - 19,176.65. + council_tax_reduction_applicable_income: 48_160.75 + # 15,000 - 20% x (48,160.75 - 4,968.60): the reduction rises by 4.69. + council_tax_reduction: 6_361.57 diff --git a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py index 031f5b855e..ade0b6792c 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py @@ -13,44 +13,48 @@ Neither counts the Scottish Carer Supplement, paid with Carer Support Payment from 15 March 2026: reg 57(1) is a closed list that leaves it out, and reg 27(1)(j)(xxib) excepts it. Nor does the tax on the supplement count: at -working age SSI 2021/249 deducts income tax only from earnings (Part 6 -Chapter 3), and at pension age SSI 2012/319 reg 31(12) disregards the tax -payable on income taken into account, which the supplement is not. So in law -the supplement does not touch Council Tax Reduction at all. The model deducts -all of a family's income tax from its Council Tax Reduction income, an -existing approximation, so there the supplement, which is taxable (SI -2026/93), reaches that income through the tax charged on it and in no other -way. The tax deducted is the liability before the winter fuel payment charge -(ITEPA 2003 s. 681I), which recovers a winter payment rather than taxing the -income assessed; "income tax" below means that liability. +working age the Scottish Ministers let the council deduct income tax only from +earnings (SSI 2021/249 Part 6 Chapter 3), and at pension age the council +disregards the tax payable on income taken into account (SSI 2012/319 reg +31(12)), which the supplement is not. The supplement is taxable (SI 2026/93). +The model deducts the tax and National Insurance each scheme allows +(council_tax_reduction_tax_deduction). At working age that includes the extra +tax earnings cause, with the earnings as the top slice of income (the choice +settled for Pension Credit on PolicyEngine/policyengine-uk#2019, option (a)), +so the supplement beneath the earnings can raise the tax the earnings add. At +pension age the formula deducts the tax on the counted income with the +supplement above it. Invariants, for single people and couples in Scotland in 2026 in which one member is a carer, all of working age and taking no Universal Credit or all over State Pension age: 1. Counts the component: claiming Carer Support Payment raises Council Tax - Reduction income, before income tax and National Insurance and apart from - the other benefits it counts, by exactly the Carer Support Payment + Reduction income, before the income tax and National Insurance it deducts + and apart from the other benefits it counts, by exactly the Carer Support Payment component (86.45 a week), not by the component plus the supplement, and by that amount before any overlapping-benefit reduction (see below). The reduction itself never rises, and where both awards are partial it falls by 20% of the rise in income less the rise in the applicable amount (the carer premium comes with the claim). -2. Invariant to the supplement: setting the Scottish Carer Supplement to zero - leaves Carer Support Payment and the other benefits counted in Council - Tax Reduction income (Child Benefit, the income-related benefits, - Universal Credit and tax credits) unchanged, and changes that income only - through income tax: income plus income tax is unchanged. The supplement - never lowers the reduction, and where both awards are partial it raises - it by 20% of the tax on the supplement. +2. Invariant to the supplement, apart from the tax earnings add: setting the + Scottish Carer Supplement to zero leaves Carer Support Payment and the + other benefits counted in Council Tax Reduction income (Child Benefit, the + income-related benefits, Universal Credit and tax credits) unchanged. + Under the general rules it changes Council Tax Reduction income only by + the change in the deduction for income tax and National Insurance, and in + a family with no earnings it changes neither that income nor the + reduction. The supplement never lowers the reduction. 3. Differential: Council Tax Reduction and Housing Benefit assess the same carer income. Where Guarantee Credit does not passport Housing Benefit, Housing Benefit applicable income, before its disregard, childcare element and tariff income, equals Council Tax Reduction income less the part of Carer Support Payment that an overlapping benefit removes, which Housing - Benefit counts after the reduction (HB (SPC) Regs reg 29(4)(g)). This holds in - Scotland, where the carer's benefit is Carer Support Payment, and in - England and Wales, where it is Carer's Allowance. + Benefit counts after the reduction (HB (SPC) Regs reg 29(4)(g)), and less + the income tax and National Insurance that Housing Benefit deducts and + Council Tax Reduction does not. This holds in Scotland, where the carer's + benefit is Carer Support Payment, and in England and Wales, where it is + Carer's Allowance. Invariants 1 to 3 concern Council Tax Reduction income under the general rules. A pension-age family in receipt of a guarantee credit has its whole income @@ -84,9 +88,11 @@ CARER_SUPPORT_PAYMENT = 86.45 * WEEKS SCOTTISH_CARER_SUPPLEMENT = 11.70 * WEEKS WITHDRAWAL_RATE = 0.2 -# The income tax Council Tax Reduction income deducts: the liability before -# the winter fuel payment charge. +# The income tax Housing Benefit deducts: the liability before the winter fuel +# payment charge. INCOME_TAX = "income_tax_before_winter_fuel_payment_charge" +# The income tax and National Insurance Council Tax Reduction income deducts. +CTR_TAX = "council_tax_reduction_tax_deduction" PROPERTY_SETTINGS = settings( max_examples=10, deadline=None, @@ -114,6 +120,7 @@ ] BENUNIT_VARIABLES = [ "council_tax_reduction_applicable_income", + CTR_TAX, "council_tax_reduction_applicable_amount", "council_tax_benefit", "guarantee_credit", @@ -246,8 +253,7 @@ def grid(families, reform=None): # from the other benefits it counts. g["income_before_tax"] = ( g["council_tax_reduction_applicable_income"] - + g[INCOME_TAX] - + g["national_insurance"] + + g[CTR_TAX] - g["other_counted_benefits"] ) # Cells whose Council Tax Reduction income follows the general rules, not @@ -340,29 +346,28 @@ def test_council_tax_reduction_income_ignores_the_scottish_carer_supplement( ) income = g["council_tax_reduction_applicable_income"][i] income_without = without["council_tax_reduction_applicable_income"][i] - # Only where the zero floor on income does not bind in either run, and - # both runs follow the general rules. + # Where both runs follow the general rules. general = g["general_rules"][i] & without["general_rules"][i] - compared = (income > 0) & (income_without > 0) & general - assert compared.any(), fam + assert general.any(), fam + # The supplement reaches the income only through the deduction. assert np.allclose( - (income + g[INCOME_TAX][i])[compared], - (income_without + without[INCOME_TAX][i])[compared], - atol=0.05, - ), fam + (income + g[CTR_TAX][i])[general], + (income_without + without[CTR_TAX][i])[general], + atol=0.01, + ), (fam, income[general], income_without[general]) + has_earnings = any(a.get("employment_income", 0.0) > 0 for a in fam["adults"]) award = g["council_tax_benefit"][i] award_without = without["council_tax_benefit"][i] - # The supplement can only add tax, which lowers income, so it never - # lowers the award, even on the Pension Credit routes (whose income - # follows invariant 4). + # The supplement never lowers the award, even on the Pension Credit + # routes (whose income follows invariant 4). assert np.all(award >= award_without - 0.01), fam - both_partial = partial(g, i) & partial(without, i) & general - tax_on_supplement = g[INCOME_TAX][i] - without[INCOME_TAX][i] - assert np.allclose( - (award - award_without)[both_partial], - WITHDRAWAL_RATE * tax_on_supplement[both_partial], - atol=0.05, - ), fam + if not has_earnings: + assert np.allclose(income[general], income_without[general], atol=0.01), ( + fam, + income[general], + income_without[general], + ) + assert np.allclose(award[general], award_without[general], atol=0.01), fam # Without a claim there is no supplement, so nothing moves. assert np.allclose(income[:, 1], income_without[:, 1], atol=0.01), fam @@ -400,7 +405,12 @@ def test_council_tax_reduction_and_housing_benefit_assess_the_same_carer_income( + g["housing_benefit_applicable_income_childcare_element"][i] - g["housing_benefit_tariff_income"][i] ) - ctr_less_overlap = ctr_income - removed_by_overlap + # Housing Benefit deducts the whole liability; Council Tax Reduction + # the tax and National Insurance its scheme allows. + tax_only_hb_deducts = ( + g[INCOME_TAX][i] + g["national_insurance"][i] - g[CTR_TAX][i] + ) + ctr_less_overlap = ctr_income - removed_by_overlap - tax_only_hb_deducts assert np.allclose( hb_before_adjustments[compared], ctr_less_overlap[compared], atol=0.05 ), (fam, hb_before_adjustments[compared], ctr_less_overlap[compared]) diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py index 9afdf55140..5c4458290c 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -304,12 +304,16 @@ def situation( "esa_income", "universal_credit", ] -# The means tests deduct the income tax before the winter fuel payment charge -# (ITEPA 2003 s.681I), less the tax on income they do not take into account -# due without earnings (legacy_means_test_deducted_income_tax): the charge -# recovers a payment rather than taxing the income they assess. A pensioner -# over £35,000 of total income is charged it. -MAIN_DEDUCTIONS = ["legacy_means_test_deducted_income_tax", "national_insurance"] +# The income tax and National Insurance each scheme allows +# (council_tax_reduction_tax_deduction), a benefit unit total for the +# applicant and partner. It starts from income tax before the winter fuel +# payment charge (ITEPA 2003 s.681I), because the charge recovers a payment +# rather than taxing the income the schemes assess; a pensioner over £35,000 +# of total income pays it. The deduction's own tests +# (test_council_tax_reduction_tax_deduction_properties.py and +# council_tax_reduction_tax_deduction.yaml) check it against the law; this file +# checks the routes around it. +MAIN_DEDUCTIONS = ["council_tax_reduction_tax_deduction"] # The pensioner earnings disregards in 2026, written out from the law, not # read from the parameters: £25 a week for a lone parent, £10 with a partner, # £5 otherwise, plus £17.10 (SI 2012/2885 Sch 4 paras 2, 8 and 10; WSI @@ -392,7 +396,7 @@ def benunit_total(names): benunit_total(["child_benefit"]), 0, ) - - benunit_total(MAIN_DEDUCTIONS) + - sum(np.asarray(sim.calculate(v, YEAR))[claimants] for v in MAIN_DEDUCTIONS) - 0.5 * benunit_total(["pension_contributions"]) - pensioner_earnings_disregard(sim, claimants), ) diff --git a/policyengine_uk/tests/test_council_tax_reduction_tax_deduction_properties.py b/policyengine_uk/tests/test_council_tax_reduction_tax_deduction_properties.py new file mode 100644 index 0000000000..cda3059434 --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_tax_deduction_properties.py @@ -0,0 +1,731 @@ +"""Income tax and National Insurance deducted from Council Tax Reduction income. + +In Scotland's working-age scheme (the Council Tax Reduction (Scotland) +Regulations 2021, SSI 2021/249), the Scottish Ministers let the council take +income tax and primary Class 1 contributions off earnings only (regs 49(6)(b), +50(3)(a) and 51(2)), and give no tax deduction against unearned income (regs 57 +to 65). Without Universal Credit, in reg 51(9) they apply the notional tax and +contributions of SSI 2012/303 reg 38 to self-employed profit. In the pensioner +schemes, the council disregards tax only on income taken into account and on +earnings: SSI 2012/319 regs 31(12), 33(2)(a) and 37 in Scotland, SI 2012/2885 +Sch 1 paras 17(13), 19(2)(a) and 30 in England, and WSI 2013/3029 Sch 1 paras +11(13), 13(2)(a) and 24 in Wales. + +No provision says how to split one income tax liability. The model follows +the choice settled for Pension Credit on PolicyEngine/policyengine-uk#2019 +(option (a)): the tax on earnings is the extra tax the earnings cause, with +the earnings as the top slice of income. council_tax_reduction_tax_deduction +applies these rules, and council_tax_reduction_applicable_income deducts it. +In the English and Welsh working-age schemes, the formula deducts what the +legacy means tests deduct (legacy_means_test_deducted_income_tax). + +Each test builds households by hand in 2026, states its invariant in its +docstring, and records the scheme of each household it draws as a Hypothesis +event, so that `--hypothesis-show-statistics` counts the examples per scheme. +""" + +import numpy as np +from hypothesis import HealthCheck, event, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +REGIONS = {"ENGLAND": "NORTH_WEST", "WALES": "WALES", "SCOTLAND": "SCOTLAND"} +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +DEDUCTION = "council_tax_reduction_tax_deduction" +INCOME_TAX = "income_tax_before_winter_fuel_payment_charge" +NO_SCOTTISH_CARER_SUPPLEMENT = { + "gov.social_security_scotland.carer_support_payment.supplement": { + "2026-01-01.2100-12-31": 0 + } +} +# Taxed income the pensioner schemes do not count, as inputs. +UNCOUNTED = [ + "savings_interest_income", + "other_investment_income", + "dividend_income", + "miscellaneous_income", + "company_car_benefit", +] +# The same with the profits of a property business, which the strategies below +# draw on their own. +PENSIONER_UNCOUNTED = UNCOUNTED + ["property_income"] +# Taxed income other than earnings that Scotland's working-age scheme counts +# or treats as capital. +UNEARNED = [ + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", + "miscellaneous_income", +] + + +def money(high): + return st.floats(0, high, allow_nan=False, allow_infinity=False) + + +def maybe(high): + return st.one_of(st.just(0.0), money(high)) + + +def simulate(households, reform=None): + """One simulation of households. Each household is a dict with country, + benunits (a list of lists of person input dicts; the first benunit is + the Council Tax Reduction applicant's) and, optionally, uc, + child_benefit (whether the applicant's benefit unit claims Child + Benefit) and council_tax (2,000 where the household gives none).""" + situation = {"people": {}, "benunits": {}, "households": {}} + for h, household in enumerate(households): + names = [] + for b, adults in enumerate(household["benunits"]): + members = [] + for p, inputs in enumerate(adults): + name = f"p{h}_{b}_{p}" + situation["people"][name] = {k: {YEAR: v} for k, v in inputs.items()} + members.append(name) + situation["benunits"][f"b{h}_{b}"] = { + "members": members, + "would_claim_uc": {YEAR: household.get("uc", False)}, + } + if b == 0 and "child_benefit" in household: + situation["benunits"][f"b{h}_{b}"]["would_claim_child_benefit"] = { + YEAR: household["child_benefit"] + } + names += members + country = household["country"] + situation["households"][f"h{h}"] = { + "members": names, + "country": {YEAR: country}, + "region": {YEAR: REGIONS[country]}, + "council_tax": {YEAR: household.get("council_tax", 2_000.0)}, + "savings": {YEAR: 0.0}, + } + return Simulation(situation=situation, reform=reform) + + +def benunit_values(simulation, variable): + return np.asarray(simulation.calculate(variable, YEAR)) + + +def applicant_total(simulation, variable): + """The Council Tax Reduction applicant's and partner's total, by benunit.""" + values = np.asarray(simulation.calculate(variable, YEAR)) + members = np.asarray( + simulation.calculate("is_council_tax_reduction_applicant_or_partner", YEAR) + ) + return np.asarray( + simulation.map_result(values * members, "person", "benunit", how="sum") + ) + + +def scheme_of(country, pensioner, uc=False): + if pensioner: + return f"{country.lower()} pensioner" + if country == "SCOTLAND": + return "scotland working age " + ("with UC" if uc else "without UC") + return f"{country.lower()} working age" + + +@st.composite +def adult(draw, pensioner, earnings=True, self_employment=True): + person = dict( + age=draw(st.integers(67, 90)) if pensioner else draw(st.integers(25, 60)), + employment_income=draw(maybe(90_000)) if earnings else 0.0, + self_employment_income=draw(maybe(90_000)) if self_employment else 0.0, + private_pension_income=draw(maybe(60_000)), + property_income=draw(maybe(30_000)), + **{v: draw(maybe(40_000)) for v in UNCOUNTED}, + ) + if pensioner: + person["state_pension"] = draw(money(12_000)) + return person + + +@st.composite +def household(draw, countries=tuple(REGIONS), pensioner=None, **kwargs): + country = draw(st.sampled_from(countries)) + is_pensioner = draw(st.booleans()) if pensioner is None else pensioner + adults = draw(st.lists(adult(is_pensioner, **kwargs), min_size=1, max_size=2)) + h = dict(country=country, benunits=[adults], pensioner=is_pensioner) + if country == "SCOTLAND" and not is_pensioner and draw(st.booleans()): + # A Universal Credit claim with two children, and earnings, profit, + # pension and property income low enough for an award. Savings + # interest, dividends and the other uncounted income stay as drawn: + # Universal Credit does not count them. + h["uc"] = True + for a in adults: + for variable in ["employment_income", "self_employment_income"]: + a[variable] = min(a[variable], 10_000.0) + a["private_pension_income"] = 0.0 + a["property_income"] = 0.0 + adults += [dict(age=4), dict(age=7)] + return h + + +def grown_ups(people): + """Indices of the adults among a benefit unit's members.""" + return [i for i, p in enumerate(people) if p["age"] >= 18] + + +def schemes(simulation, households): + pensioner = benunit_values(simulation, "council_tax_reduction_pensioner") + universal_credit = benunit_values(simulation, "universal_credit") > 0 + labels = [] + for i, h in enumerate(households): + assert pensioner[i] == h["pensioner"], h + labels.append(scheme_of(h["country"], pensioner[i], universal_credit[i])) + event(labels[-1]) + return labels + + +@PROPERTY_SETTINGS +@given( + st.lists( + household(countries=("SCOTLAND",), pensioner=False, self_employment=False), + min_size=1, + max_size=4, + ) +) +def test_scottish_working_age_deduction_matches_pension_credit_earnings_tax( + households, +): + """Invariant 1. At working age in Scotland without self-employment income, + the deduction less primary Class 1 contributions equals the income tax on + employed earnings that Pension Credit computes + (pension_credit_employment_income_tax), wherever the Council Tax + Reduction applicant and partner are the Pension Credit claimant and + partner.""" + simulation = simulate(households) + schemes(simulation, households) + same_people = np.asarray( + simulation.calculate("is_council_tax_reduction_applicant_or_partner", YEAR) + ) == np.asarray(simulation.calculate("is_claimant_or_partner", YEAR)) + assert same_people.all() + deduction = benunit_values(simulation, DEDUCTION) + class_1 = applicant_total(simulation, "ni_class_1_employee") + pension_credit = benunit_values(simulation, "pension_credit_employment_income_tax") + assert np.allclose(deduction - class_1, pension_credit, atol=0.01), ( + households, + deduction, + class_1, + pension_credit, + ) + + +@PROPERTY_SETTINGS +@given( + st.lists(household(pensioner=True, earnings=False), min_size=1, max_size=4), + st.sampled_from(PENSIONER_UNCOUNTED), + money(60_000), + st.booleans(), +) +def test_pensioner_deduction_ignores_uncounted_income_without_earnings( + households, uncounted, change, partner +): + """Invariant 2. In every pensioner scheme, with no employed earnings, the + deduction does not change when any uncounted income changes, for either + member of a couple. The notional amount on self-employed profit reads the + personal reliefs to which the person is entitled, which the uncounted + income can taper, so the test compares the deduction less that amount.""" + changed = [] + for h in households: + adults = [dict(a) for a in h["benunits"][0]] + who = grown_ups(adults)[-1] if partner else 0 + adults[who][uncounted] = adults[who][uncounted] + change + changed.append({**h, "benunits": [adults]}) + before = simulate(households) + after = simulate(changed) + schemes(before, households) + + def without_notional(simulation): + notional = np.where( + np.asarray([h["country"] == "SCOTLAND" for h in households], dtype=bool), + applicant_total( + simulation, "council_tax_reduction_scotland_self_employment_deductions" + ), + applicant_total(simulation, "pension_credit_self_employment_deductions"), + ) + return benunit_values(simulation, DEDUCTION) - notional + + assert np.allclose(without_notional(before), without_notional(after), atol=0.01), ( + households, + uncounted, + change, + without_notional(before), + without_notional(after), + ) + + +@st.composite +def pensioner_parent_household(draw): + """A pensioner family with one or two children and no employed earnings, + each adult drawing a private pension of up to 120,000, so that the + applicant's adjusted net income often exceeds the 60,000 at which the High + Income Child Benefit Charge starts.""" + h = draw(household(pensioner=True, earnings=False)) + adults = h["benunits"][0] + for a in adults: + a["private_pension_income"] = draw(money(120_000)) + children = [dict(age=draw(st.integers(0, 15)))] + if draw(st.booleans()): + children.append(dict(age=draw(st.integers(0, 15)))) + return {**h, "benunits": [adults + children]} + + +@PROPERTY_SETTINGS +@given(st.lists(pensioner_parent_household(), min_size=1, max_size=4)) +def test_pensioner_deduction_ignores_a_child_benefit_claim(households): + """Invariant 2, for Child Benefit. In every pensioner scheme, with no + employed earnings, the deduction does not change with a claim for Child + Benefit. The pensioner schemes do not count Child Benefit, and through the + High Income Child Benefit Charge HMRC recovers it, so the formula treats + the charge as not tax payable on income taken into account. The test + compares two simulations of the same families, one claiming Child Benefit + and one not.""" + claiming = [{**h, "child_benefit": True} for h in households] + not_claiming = [{**h, "child_benefit": False} for h in households] + with_claim = simulate(claiming) + without_claim = simulate(not_claiming) + schemes(with_claim, households) + charge = applicant_total(with_claim, "CB_HITC") + for charged in charge > 0: + event("High Income Child Benefit Charge" if charged else "no charge") + assert np.all(benunit_values(with_claim, "child_benefit") > 0) + assert np.all(benunit_values(without_claim, "child_benefit") == 0) + assert np.allclose( + benunit_values(with_claim, DEDUCTION), + benunit_values(without_claim, DEDUCTION), + atol=0.01, + ), ( + households, + charge, + benunit_values(with_claim, DEDUCTION), + benunit_values(without_claim, DEDUCTION), + ) + + +@st.composite +def any_household(draw): + return draw(household()) + + +@PROPERTY_SETTINGS +@given(st.lists(any_household(), min_size=1, max_size=4)) +def test_deduction_bounds(households): + """Invariant 3. The deduction never falls below zero. Without + self-employment income it never exceeds the applicant's and partner's + income tax before the winter fuel payment charge plus National Insurance. + With it, the notional amount on profit can take the deduction above that + liability, but never above the liability plus the notional amount.""" + simulation = simulate(households) + schemes(simulation, households) + deduction = benunit_values(simulation, DEDUCTION) + whole = applicant_total(simulation, INCOME_TAX) + applicant_total( + simulation, "national_insurance" + ) + profit = applicant_total(simulation, "self_employment_income") + notional = np.maximum( + applicant_total( + simulation, "council_tax_reduction_scotland_self_employment_deductions" + ), + applicant_total(simulation, "pension_credit_self_employment_deductions"), + ) + assert np.all(deduction >= 0), (households, deduction) + assert np.all((deduction <= whole + 0.01)[profit == 0]), ( + households, + deduction, + whole, + ) + assert np.all(deduction <= whole + notional + 0.01), ( + households, + deduction, + whole, + notional, + ) + + +@PROPERTY_SETTINGS +@given( + st.lists( + household( + countries=("SCOTLAND",), + pensioner=False, + earnings=False, + self_employment=False, + ), + min_size=1, + max_size=4, + ) +) +def test_scottish_working_age_deducts_nothing_without_earnings(households): + """Invariant 4. At working age in Scotland, with no earnings and no + profit, the deduction is nil, however much other taxed income there is.""" + simulation = simulate(households) + schemes(simulation, households) + deduction = benunit_values(simulation, DEDUCTION) + assert np.all(deduction == 0), (households, deduction) + + +@st.composite +def household_with_council_tax(draw, **kwargs): + """A household from household(), with council tax of up to 30,000, so that + families keep a reduction across the whole range of pay.""" + return {**draw(household(**kwargs)), "council_tax": draw(money(30_000))} + + +# The Hypothesis event invariant 5 records for each cliff or step it holds. +CROSSES = { + "marriage_allowance": "crosses the Marriage Allowance cliff", + "savings_allowance": "crosses a personal savings allowance cliff", + "CB_HITC": "crosses a High Income Child Benefit Charge step", + "personal_allowance": "crosses a personal allowance step", +} +# Invariant 5 runs these three families every time. A lone parent with two +# children whose pay of 10,000 rises by 1 takes adjusted net income from +# 64,999.50 to 65,000.50, across a step of the High Income Child Benefit +# Charge (560 to 584). +LONE_PARENT_AT_A_CHARGE_STEP = dict( + country="SCOTLAND", + pensioner=False, + council_tax=2_000.0, + benunits=[ + [ + dict( + age=40, + employment_income=10_000.0, + savings_interest_income=40_000.0, + other_investment_income=14_999.50, + ), + dict(age=4), + dict(age=7), + ] + ], +) +# A single person with pay of 90,000 and savings interest of 20,001.75, whose +# pay rises by 0.50, takes adjusted net income across 110,002, where the +# tapered personal allowance falls from 7,570 to 7,569. +SINGLE_PERSON_AT_A_PERSONAL_ALLOWANCE_STEP = dict( + country="SCOTLAND", + pensioner=False, + council_tax=30_000.0, + benunits=[ + [dict(age=40, employment_income=90_000.0, savings_interest_income=20_001.75)] + ], +) +# A single person whose pay rises from 13,000 to 14,000, within the Scottish +# starter band: the deduction rises by 190 of income tax and 80 of Class 1. +SINGLE_PERSON_IN_THE_STARTER_BAND = dict( + country="SCOTLAND", + pensioner=False, + council_tax=2_000.0, + benunits=[[dict(age=40, employment_income=13_000.0)]], +) + + +@PROPERTY_SETTINGS +@given( + st.lists( + household_with_council_tax(countries=("SCOTLAND",), pensioner=False), + min_size=1, + max_size=4, + ), + money(20_000), + st.booleans(), +) +@example(households=[LONE_PARENT_AT_A_CHARGE_STEP], rise=1.0, partner=False) +@example( + households=[SINGLE_PERSON_AT_A_PERSONAL_ALLOWANCE_STEP], rise=0.5, partner=False +) +@example(households=[SINGLE_PERSON_IN_THE_STARTER_BAND], rise=1_000.0, partner=False) +def test_scottish_working_age_reduction_never_rises_with_pay(households, rise, partner): + """Invariant 5. At working age in Scotland without Universal Credit, + raising either member's employment income never raises Council Tax + Reduction once the test holds the income tax system's cliffs and steps + still. Option (a) passes each of them through to the deduction, and each + can raise the tax the earnings add by more than the rise in pay: + - Marriage Allowance: an earner who enters the higher band loses the + allowance, which the model takes off taxable income. A Scottish couple, + one partner with no income and council tax of 15,000, whose pay rises + from 43,661 to 43,663 loses 1,260 of it: the deduction rises by 265.39 + and the reduction by 52.68 (the YAML cases pin both). + - The personal savings allowance: it falls from 1,000 to 500 as a person + enters the higher band, and to nil in the additional band, so a pound + of pay can bring 500 of savings income into tax at 40%, adding 200. + - The High Income Child Benefit Charge: the model charges a whole + percentage point of the Child Benefit for each whole 200 of adjusted + net income above 60,000, so a pound of pay that crosses a step raises + the charge by a percent of the Child Benefit, 23 or 24 pounds for two + children. + - The personal allowance: the model rounds the tapered allowance up to a + whole pound, so a rise in pay of less than 2 can lower the allowance by + a pound, and the tax then rises on the rise plus that pound. + The test holds marriage_allowance, savings_allowance, CB_HITC and + personal_allowance at their values before the rise in two more + simulations, one of the families before the rise and one of the raised + families, and asserts, for every family, that the reduction in the second + does not exceed the reduction in the first. Because + income_tax_with_inputs_zeroed keeps stored inputs on its branches, the + branch without the pay then reads the same held values on both sides. The + test draws council tax of up to 30,000, and always runs three families: + a lone parent who crosses a step of the charge, a single person who + crosses a step of the personal allowance, and a single person in the + starter band.""" + raised = [] + for h in households: + adults = [dict(a) for a in h["benunits"][0]] + who = grown_ups(adults)[-1] if partner else 0 + adults[who]["employment_income"] += rise + raised.append({**h, "uc": False, "benunits": [adults]}) + # The invariant is for families without Universal Credit. + households = [{**h, "uc": False} for h in households] + before = simulate(households) + after = simulate(raised) + # income_tax_with_inputs_zeroed keeps stored inputs on its branches, so + # hold the cliffs and steps on both sides. + held_before = simulate(households) + held = simulate(raised) + cliffs = [ + "marriage_allowance", + "savings_allowance", + "CB_HITC", + "personal_allowance", + ] + for variable in cliffs: + values = np.asarray(before.calculate(variable, YEAR)) + held_before.set_input(variable, YEAR, values) + held.set_input(variable, YEAR, values) + schemes(before, households) + reduction_held_before = np.asarray( + held_before.calculate("council_tax_reduction", YEAR) + ) + reduction_held = np.asarray(held.calculate("council_tax_reduction", YEAR)) + # The model moves each of these in steps, so any change crosses one. + crossed_any = np.zeros(len(households), dtype=bool) + for variable in cliffs: + crossed = ~np.isclose( + applicant_total(before, variable), applicant_total(after, variable) + ) + crossed_any |= crossed + if crossed.any(): + event(CROSSES[variable]) + if not crossed_any.all(): + event("crosses no cliff or step") + assert np.all(reduction_held <= reduction_held_before + 0.01), ( + households, + rise, + partner, + reduction_held_before, + reduction_held, + ) + + +@PROPERTY_SETTINGS +@given( + st.lists(any_household(), min_size=1, max_size=3), + adult(pensioner=False), + adult(pensioner=False), +) +def test_non_dependant_income_never_changes_the_deduction( + households, non_dependant, other_income +): + """Invariant 6. A non-dependant's income never changes the deduction: + adding a working-age adult with their own benefit unit to the household, + and changing that adult's income, leaves the applicant's deduction where + it was.""" + with_non_dependant = [ + {**h, "benunits": h["benunits"] + [[non_dependant]]} for h in households + ] + changed = [{**h, "benunits": h["benunits"] + [[other_income]]} for h in households] + alone = simulate(households) + schemes(alone, households) + first = simulate(with_non_dependant) + second = simulate(changed) + # The applicant's benunit is the first in each household. + applicant = np.arange(len(households)) * 2 + deduction = benunit_values(alone, DEDUCTION) + for simulation in (first, second): + assert np.allclose( + benunit_values(simulation, DEDUCTION)[applicant], deduction, atol=0.01 + ), (households, non_dependant, other_income) + + +CASES = [ + dict( + country="SCOTLAND", + benunits=[[dict(age=40, employment_income=30_000.0, dividend_income=9_000.0)]], + ), + dict( + country="SCOTLAND", benunits=[[dict(age=40, self_employment_income=15_000.0)]] + ), + dict( + country="SCOTLAND", + uc=True, + benunits=[ + [ + dict(age=35, self_employment_income=15_000.0), + dict(age=5), + dict(age=3), + ] + ], + ), + dict( + country="SCOTLAND", benunits=[[dict(age=70, private_pension_income=15_000.0)]] + ), + dict( + country="ENGLAND", + benunits=[[dict(age=70, employment_income=20_000.0)]], + ), + dict( + country="WALES", + benunits=[[dict(age=70, self_employment_income=60_000.0)]], + ), + dict( + country="WALES", + benunits=[[dict(age=40, employment_income=20_000.0)]], + ), +] + + +def test_council_tax_reduction_is_the_same_whichever_variable_is_calculated_first(): + """Invariant 7. Council Tax Reduction and its tax deduction come out the + same whether the deduction or the reduction is calculated first.""" + variables = [ + DEDUCTION, + "council_tax_reduction_applicable_income", + "council_tax_reduction", + ] + for case in CASES: + applicant = case["benunits"][0][0] + inputs = { + **case, + "benunits": [ + [{**applicant, "savings_interest_income": 6_559.0}] + + case["benunits"][0][1:] + ], + } + results = [] + for order in (variables, variables[::-1]): + simulation = simulate([inputs]) + results.append( + {v: float(simulation.calculate(v, YEAR).sum()) for v in order} + ) + assert results[0] == results[1], (case, results) + + +UNCOUNTED_CASES = [ + ("savings_interest_income", 10_000.0), + ("other_investment_income", 10_000.0), + ("dividend_income", 10_000.0), + ("miscellaneous_income", 10_000.0), + ("company_car_benefit", 10_000.0), + ("property_income", 10_000.0), +] + + +def test_branch_takes_each_uncounted_income_out_of_income_tax(): + """The pensioner schemes' tax on other counted income zeroes each income + they do not count, so it equals the income tax of the same person without + that income. This covers the Scottish Carer Supplement, which the model + computes from the carer's award rather than reading as an input.""" + base = dict(age=70, private_pension_income=30_000.0) + with_income = [ + dict(country="ENGLAND", benunits=[[{**base, variable: amount}]]) + for variable, amount in UNCOUNTED_CASES + ] + without_income = [dict(country="ENGLAND", benunits=[[base]])] + branch = np.asarray( + simulate(with_income).calculate( + "council_tax_reduction_income_tax_on_other_counted_income", YEAR + ) + ) + expected = float(simulate(without_income).calculate(INCOME_TAX, YEAR)[0]) + assert np.allclose(branch, expected, atol=0.01), (branch, expected) + tax_with_income = np.asarray(simulate(with_income).calculate(INCOME_TAX, YEAR)) + assert np.all(tax_with_income > expected + 1), tax_with_income + + # A landlord's finance costs earn a 20% tax reduction on the rent. The + # branch drops the reduction with the rent, so it gives the same tax with + # or without the costs. + landlords = simulate( + [ + dict( + country="ENGLAND", + benunits=[ + [ + { + **base, + "property_income": 10_000.0, + "property_finance_costs": costs, + } + ] + ], + ) + for costs in (0.0, 5_000.0) + ] + ) + landlord_tax = np.asarray(landlords.calculate(INCOME_TAX, YEAR)) + assert np.isclose(landlord_tax[0] - landlord_tax[1], 1_000, atol=0.01) + assert np.allclose( + landlords.calculate( + "council_tax_reduction_income_tax_on_other_counted_income", YEAR + ), + expected, + atol=0.01, + ) + + carer = dict( + age=70, + private_pension_income=14_000.0, + care_hours=35, + would_claim_carers_allowance=True, + ) + carer_household = [dict(country="SCOTLAND", benunits=[[carer]])] + with_supplement = simulate(carer_household) + without_supplement = simulate(carer_household, reform=NO_SCOTTISH_CARER_SUPPLEMENT) + assert with_supplement.calculate("scottish_carer_supplement", YEAR)[0] > 0 + assert np.isclose( + with_supplement.calculate( + "council_tax_reduction_income_tax_on_other_counted_income", YEAR + )[0], + without_supplement.calculate(INCOME_TAX, YEAR)[0], + atol=0.01, + ) + assert ( + with_supplement.calculate(INCOME_TAX, YEAR)[0] + > without_supplement.calculate(INCOME_TAX, YEAR)[0] + 1 + ) + + +@PROPERTY_SETTINGS +@given( + st.lists( + household(countries=("ENGLAND", "WALES"), pensioner=False), + min_size=1, + max_size=4, + ) +) +def test_other_schemes_deduct_the_legacy_means_test_measure(households): + """Invariant 8. In the English and Welsh working-age schemes, the + deduction equals what the legacy means tests deduct: the applicant's and + partner's legacy_means_test_deducted_income_tax, with the sum floored at + nil, plus their National Insurance. So these schemes take no tax on the + interest, dividends or rent from property that they do not count, beyond + the extra tax that earnings cause on it.""" + simulation = simulate(households) + schemes(simulation, households) + expected = np.maximum( + 0, applicant_total(simulation, "legacy_means_test_deducted_income_tax") + ) + applicant_total(simulation, "national_insurance") + deduction = benunit_values(simulation, DEDUCTION) + assert np.allclose(deduction, expected, atol=0.01), ( + households, + deduction, + expected, + ) diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 550cf296fd..d0ac72194a 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -27,9 +27,11 @@ personal allowance can leave rent or dividends taxable. Where someone has earnings, each means-test income changes by exactly minus the change in the tax that test deducts (legacy_means_test_deducted_income_tax, summed - over the members it counts) and in its earnings disregard, which is - capped at net earnings, unless floored or on a route that does not use - the test's own income rules. The awards are compared wherever none of + over the members it counts; for council tax reduction, + council_tax_reduction_tax_deduction, which applies each scheme's own + rule) and in its earnings disregard, which never exceeds net earnings, + unless a floor binds or the family takes a route that does not use the + test's own income rules. The test compares the awards wherever none of those taxes or disregards moves. 2. Bounds: the counted home-letting income is between nil and the rent, and equals the rent less £20 a week, floored at nil. @@ -59,10 +61,16 @@ savings rise while the family stays on the same route (pensioner, UC award, passport, guarantee credit, savings credit only). 6. Differential: for every generated family without self-employment income, - Income Support, Housing Benefit and the council tax reduction line deduct, - for each person they count, the same income tax as Pension Credit's tax - line: the pre-charge income tax less - legacy_means_test_uncounted_income_tax_without_earnings. + Income Support and Housing Benefit deduct, for each person they count, the + same income tax as Pension Credit's tax line: the pre-charge income tax + less legacy_means_test_uncounted_income_tax_without_earnings. The council + tax reduction line deducts council_tax_reduction_tax_deduction, which + deducts that tax only in the schemes it leaves to the legacy means tests + (the English and Welsh working-age schemes and outside Great Britain). + In Scotland's working-age scheme and Great Britain's pensioner schemes it + applies those schemes' own rules + (test_council_tax_reduction_tax_deduction_properties.py), so there the + line does not read Pension Credit's tax. 7. Applicant: where the household head does not apply alone for council tax reduction, council_tax_reduction_home_letting_income equals legacy_benefits_home_letting_income; where the head applies alone, her @@ -266,6 +274,13 @@ def calculate_with_taxes(units, year, variables, **kwargs): for income, members in TEST_MEMBERS.items(): mask = np.asarray(sim.calculate(members, year), dtype=float) out[f"tax:{income}"] = sim.map_result(tax * mask, "person", "benunit") + # The council tax reduction line deducts each scheme's own measure, a + # benefit unit total for the applicant and partner, which equals the + # shared deduction only in the schemes it leaves to the legacy means + # tests. + out["tax:council_tax_reduction_applicable_income"] = np.asarray( + sim.calculate("council_tax_reduction_tax_deduction", year), dtype=float + ) earnings = sum( np.abs(np.asarray(sim.calculate(v, year), dtype=float)) for v in EARNINGS ) @@ -487,12 +502,17 @@ def test_deducted_tax_without_earnings_is_the_step_4_split(units, year): @PROPERTY_SETTINGS @given(populations, st.sampled_from(YEARS)) def test_the_four_tests_deduct_the_same_income_tax(units, year): - """Income Support, Housing Benefit and the council tax reduction line - deduct, for each person they count, the income tax Pension Credit's tax - line deducts. Each test's income is recomputed with the person's income - tax raised by one pound and compared with the original, so the check - reads each formula rather than the shared variable. The families have no - self-employment income, where Pension Credit's notional tax applies.""" + """Income Support and Housing Benefit deduct, for each person they count, + the income tax Pension Credit's tax line deducts, and so does the council + tax reduction line in the schemes council_tax_reduction_tax_deduction + leaves to the legacy means tests (the English and Welsh working-age + schemes and outside Great Britain). In Scotland's working-age scheme and + Great Britain's pensioner schemes, the line applies those schemes' own + rules and does not respond to that tax. The test recomputes each test's + income with the person's income tax raised by one pound and compares it + with the original, so the check reads each formula rather than the + shared variable. The families have no self-employment income, where + Pension Credit's notional tax applies.""" sim = Simulation(situation=situation(units, year)) people = sim.populations["person"].count pension_credit_tax = np.asarray( @@ -549,6 +569,15 @@ def test_the_four_tests_deduct_the_same_income_tax(units, year): ) == 0 ) + # Each family is one benefit unit in its own household. + region = np.array([unit["region"] for unit in units]) + pensioner = np.asarray( + sim.calculate("council_tax_reduction_pensioner", year) + ).astype(bool) + own_scheme_rules = (region != "NORTHERN_IRELAND") & ( + pensioner | (region == "SCOTLAND") + ) + counted = np.where(own_scheme_rules, 0.0, counted) checked = unfloored & own_rules # The earnings disregard is capped at net earnings, so it can fall # with them. diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py index 6dc0a3a7b8..e68275fdfa 100644 --- a/policyengine_uk/tests/test_trading_loss_properties.py +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -954,11 +954,15 @@ def values(sim, variable): # 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. + # Council Tax Reduction reads income tax on branches that zero earnings, + # so holding the person's tax would also hold it on those branches. Hold + # the scheme's deduction from the with-loss side instead. pc_tax = { v: values(with_loss, v) for v in [ "pension_credit_employment_income_tax", "pension_credit_self_employment_income_tax", + "council_tax_reduction_tax_deduction", ] } loss = values(with_loss, "trading_loss") diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 7b84a09992..487a1e6e43 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -43,18 +43,32 @@ class council_tax_reduction_applicable_income(Variable): "children or young persons. We disregard war pensions and Armed " "Forces Compensation Scheme payments in full: Scottish councils count " "none of them, and English and Welsh councils may relieve them beyond " - "the prescribed £10 a week. No scheme " - "counts income derived from capital, such as rent from property, " - "interest and dividends, as income, so it is not listed and tax on it " - "is not deducted (legacy_means_test_income_tax). The pensioner schemes " - "disregard any actual income from capital; the Welsh working-age " - "scheme and the English default scheme treat it as capital; the " - "Scottish working-age scheme counts only the unearned income it " - "lists, which includes the assumed yield from capital but not actual " - "rent, interest or dividends. Instead, the national schemes (England " - "for pensioners, Wales and Scotland) count the tariff income they " - "treat capital as yielding (council_tax_reduction_tariff_income); the " - "English local schemes for people under pension age add their own. " + "the prescribed £10 a week. " + "The formula counts no income derived from capital, such as rent " + "from property other than the home, interest and dividends. In each " + "pensioner scheme, a council disregards any actual income from " + "capital, except income from the capital that Part 2 of the capital " + "Schedule lists: life interests and liferents, rights to rent where " + "the applicant has no reversionary interest, annuities, and property " + "held under a trust that pays or may pay the applicant or partner. It " + "disregards that income too while such capital totals £10,000 or " + "less. The model has no input that marks capital as Part 2 capital, " + "and other_investment_income takes in interest from trusts and " + "settlements, so on the pensioner route the formula counts none of " + "the interest, other investment income, dividends or rent from " + "property, and council_tax_reduction_tax_deduction deducts no tax on " + "any of it. The model makes that general approximation of the income " + "rules: where Part 2 capital above £10,000 yields such income, a " + "council counts it and the model doesn't. The Welsh working-age " + "scheme and the English default scheme treat income from capital as " + "capital; the Scottish working-age scheme counts only the unearned " + "income it lists, which includes the assumed yield from capital but " + "not actual rent, interest or dividends. Instead, the national " + "schemes (England for pensioners, Wales and Scotland) count the " + "tariff income they treat capital as yielding " + "(council_tax_reduction_tariff_income), and in the pensioner schemes " + "a council leaves Part 2 capital out of it. The English local schemes " + "for people under pension age add their own tariff income. " "Rent the applicant and partner receive for letting part of the home " "counts, less the sub-tenant disregard, on the schemes' own income " "rules (council_tax_reduction_home_letting_income). It does not count " @@ -62,7 +76,12 @@ class council_tax_reduction_applicable_income(Variable): "whose income the schemes take from the Secretary of State's " "Universal Credit assessment (which does not count it), or in the " "Scottish scheme for people under pension age from April 2022, which " - "does not list it." + "does not list it. " + "The formula deducts the income tax and National Insurance each " + "scheme allows (council_tax_reduction_tax_deduction): on earnings " + "only in Scotland's working-age scheme, on income taken into account " + "in the pensioner schemes, and in the other schemes the income tax " + "the legacy means tests deduct (legacy_means_test_deducted_income_tax)." ) definition_period = YEAR unit = GBP @@ -103,6 +122,9 @@ class council_tax_reduction_applicable_income(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/9", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/9", "https://www.legislation.gov.uk/ssi/2012/319/schedule/3", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/4/part/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/6/part/2", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/5/part/2", "https://www.legislation.gov.uk/ssi/2012/303/regulation/26", "https://www.legislation.gov.uk/ssi/2021/249/regulation/3", "https://www.legislation.gov.uk/ssi/2021/249/regulation/63", @@ -355,15 +377,11 @@ def formula(benunit, period, parameters): pension_contributions = add_for_members( benunit, period, ["pension_contributions"], members ) * benunit("council_tax_reduction_pension_contribution_deduction_rate", period) - # The deduction can be negative for one person, where another - # member's earnings move a Step 7 charge off them; the test deducts the - # counted members' sum, floored at nil, so it never adds tax to income. - tax = max_( - 0, - add_for_members( - benunit, period, ["legacy_means_test_deducted_income_tax"], members - ), - ) + add_for_members(benunit, period, ["national_insurance"], members) + # The income tax and National Insurance each scheme allows: on earnings + # only in Scotland's working-age scheme, on income taken into account + # in the pensioner schemes, and in the other schemes what the legacy + # means tests deduct (council_tax_reduction_tax_deduction). + tax = benunit("council_tax_reduction_tax_deduction", period) # SI 2012/2885 Sch 1 para 17(9) and Sch 4; WSI 2013/3029 Sch 1 para # 11(9) and Sch 3; SSI 2012/319 reg 31(8) and Sch 2: the pensioner # earnings disregards, capped at net earnings. Zero for working-age diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_income_tax_on_other_counted_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_income_tax_on_other_counted_income.py new file mode 100644 index 0000000000..47755f34a7 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_income_tax_on_other_counted_income.py @@ -0,0 +1,140 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.pension_credit import employed_earnings_sources +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit_earnings_income_tax import ( + income_tax_with_inputs_zeroed, +) + +# Taxed income that council_tax_reduction_applicable_income leaves out on the +# pensioner schemes' general route: the Scottish Carer Supplement (SSI +# 2012/319 reg 27(1)(j)(xxib)), miscellaneous income, benefits in kind (no +# scheme counts a payment in kind as earnings), and income from capital: +# savings interest, other investment income, dividends and the profits of a +# property business (property_income), such as rent from a property the +# applicant doesn't live in. +# +# In each pensioner scheme, a council disregards any actual income from +# capital, except income from the capital that Part 2 of the capital +# Schedule lists: life interests and liferents, rights to rent where the +# applicant has no reversionary interest, annuities, and property held under +# a trust that pays or may pay the applicant or partner. It disregards that +# income too while such capital totals £10,000 or less. A council counts +# other capital through tariff income and leaves Part 2 capital out of it +# (SSI 2012/319 reg 27(1)(i) and (2), reg 31(10), Sch 3 paras 21 and 22 and +# Sch 4 Part 2; SI 2012/2885 Sch 1 paras 16(1)(i), 17(11) and 37, Sch 5 +# paras 22 and 23 and Sch 6 Part 2; WSI 2013/3029 Sch 1 paras 10(1)(i), +# 11(11) and 31, Sch 4 paras 22 and 23 and Sch 5 Part 2). So a council +# disregards the interest on savings in an ordinary bank deposit and the +# dividends on shares held outright, and counts the deposit and the shares +# through tariff income. The model has no input that marks capital as Part 2 +# capital, and other_investment_income takes in interest from trusts and +# settlements, so council_tax_reduction_applicable_income counts none of +# this income on the pensioner route, and the formula deducts no tax on any +# of it. The model makes that general approximation of the income rules: +# where Part 2 capital above £10,000 yields such income, a council counts it +# and the model doesn't. +# +# The model taxes property profits through property_income alone. Without +# them, the finance-cost reduction comes to nil on the branch, because +# property_finance_costs_relieved caps the costs it relieves at the property +# profits, so the reduction goes with the rent. property_rental_income only +# measures receipts for the property allowance, which can't take taxable +# property income below the nil the branch leaves, so the formula keeps it. +# Rent for letting part of the home (sublet_income) counts on the pensioner +# route (council_tax_reduction_home_letting_income), so the formula keeps it +# too; the model charges no income tax on it. +PENSIONER_UNCOUNTED_INCOME = [ + "scottish_carer_supplement", + "miscellaneous_income", + "benefits_in_kind", + "savings_interest_income", + "other_investment_income", + "dividend_income", + "property_income", +] + +# The Step 7 charges of ITA 2007 s. 23 that income_tax_charges holds, +# other than the winter fuel payment charge +# (gov.hmrc.income_tax.step_7_additional_tax: the High Income Child +# Benefit Charge and the pension annual allowance charge). Neither falls +# on income the pensioner schemes count: through the first, HMRC +# recovers Child Benefit, which council_tax_reduction_applicable_income +# leaves out on the pensioner route, and the second falls on pension +# contributions. +STEP_7_CHARGES = ["income_tax_charges"] + + +class council_tax_reduction_income_tax_on_other_counted_income(Variable): + value_type = float + entity = Person + label = "Income tax on the other income a pensioner Council Tax Reduction counts" + documentation = ( + "The person's income tax before the winter fuel payment charge if " + "nobody in the simulation had employed earnings (employment income " + "and statutory sick, maternity and paternity pay), self-employment " + "income, or the taxed income the pensioner schemes do not count: the " + "Scottish Carer Supplement, miscellaneous income, benefits in kind, " + "savings interest, other investment income, dividends and the profits " + "of a property business (property_income), with the finance-cost " + "reduction that goes with them. In each pensioner scheme, a council " + "disregards any actual income from capital, except income from the " + "capital that Part 2 of the capital Schedule lists: life interests " + "and liferents, rights to rent where the applicant has no " + "reversionary interest, annuities, and property held under a trust " + "that pays or may pay the applicant or partner. It disregards that " + "income too while such capital totals £10,000 or less. A council " + "counts other capital, such as " + "savings in an ordinary bank deposit or shares held outright, through " + "tariff income, and leaves Part 2 capital out of it. The model has no " + "input that marks capital as Part 2 capital, and " + "other_investment_income takes in interest from trusts and " + "settlements, so council_tax_reduction_applicable_income counts none " + "of the interest, other investment income, dividends or rent from " + "property on the pensioner route, and the formula deducts no tax on " + "any of it. The model makes that general approximation of the income " + "rules: where Part 2 capital above £10,000 yields such income, a " + "council counts it and the model doesn't. It also " + "leaves out the Step 7 charges of ITA 2007 s. 23 that " + "income_tax_charges holds, the High Income Child Benefit Charge and " + "the pension annual allowance charge: through the first, HMRC " + "recovers Child Benefit, which the pensioner schemes do not count, " + "and the second falls on pension contributions, so the formula " + "treats neither as tax payable on income taken into account. This is " + "the tax on the rest of the income the pensioner schemes count, with " + "the uncounted income above it. The " + "formula zeroes those inputs and the charges on a branch that it " + "drops afterwards, and builds the branch only when someone has " + "uncounted income or a charge." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ssi/2012/319/regulation/27", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/31", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/21", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/22", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/4/part/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/16", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/17", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/37", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/22", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/23", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/6/part/2", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/10", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/11", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/31", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/22", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/23", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/5/part/2", + ] + + def formula(person, period, parameters): + zeroed = PENSIONER_UNCOUNTED_INCOME + STEP_7_CHARGES + if not any((person(variable, period) != 0).any() for variable in zeroed): + return person("pension_credit_income_tax_without_earnings", period) + return income_tax_with_inputs_zeroed( + person.simulation, + period, + employed_earnings_sources(parameters, period) + + ["self_employment_income"] + + zeroed, + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_scotland_self_employment_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_scotland_self_employment_deductions.py new file mode 100644 index 0000000000..bf6bd79650 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_scotland_self_employment_deductions.py @@ -0,0 +1,106 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.class_2 import class_2_contribution_weeks, class_2_liable + + +class council_tax_reduction_scotland_self_employment_deductions(Variable): + value_type = float + entity = Person + label = ( + "Notional tax and contributions on self-employed profit, for Scottish " + "Council Tax Reduction" + ) + documentation = ( + "The income tax and social security contributions that a Scottish " + "council deducts from self-employed earnings for Council Tax " + "Reduction in place of those actually payable: under SSI 2012/303 " + "regs 37 and 38, which the Scottish Ministers apply in SSI 2021/249 " + "reg 51(9) at working age where nobody in the family has Universal " + "Credit, and under SSI 2012/319 regs 36 and 37 at pension age. The " + "formula charges notional income tax at the basic " + "rate (the formula reads the Scottish basic rate for a Scottish " + "taxpayer; both rates are 20%) on the profit " + "less only the personal allowance; 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, for years before 2024-25, the Class " + "2 contributions payable, which the Treasury omitted from reg 38(2) " + "and reg 37(2) from 6 April 2024 (SI 2024/377). At working age, the " + "Scottish Ministers allow the personal relief under ITA 2007 s. " + "35(1) (SSI 2012/303 reg 38(1)); the formula reads it as the " + "allowance left after the taper, because in s. 35(2) Parliament " + "reduces 'the allowance under subsection (1)'. At pension age, they " + "allow the relief under ss. 36 or 37 (SSI 2012/319 reg 37(1)). " + "Parliament omitted s. 36 for 2015-16 and later years and s. 37 for " + "2016-17 and later years, and each time extended s. 35 to the people " + "the omitted section had covered (Finance Act 2014 s. 2(4), (5) and " + "(9); Finance Act 2015 s. 5(4), (5) and (11)). A council that read " + "reg 37(1) literally would allow no applicant any relief from " + "2016-17. Where an Act " + "repeals and re-enacts a provision, Parliament provides that a " + "reference to it in subordinate legislation reads as a reference to " + "the provision re-enacted (Interpretation Act 1978 s. 17(2)(a), read " + "with s. 23(1) and (2)). The formula takes each Finance Act as " + "repealing an age-related allowance and re-enacting it in the " + "extended s. 35, so " + "it reads the reference as the tapered s. 35 allowance, which s. " + "35(1) now gives every individual. A child " + "minder's chargeable income comes to one-third of the earnings; the " + "model does not identify child minders, so it treats every profit as " + "chargeable income in full." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ssi/2021/249/regulation/51", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/37", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/38", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/36", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/37", + "https://www.legislation.gov.uk/ukpga/2007/3/section/35", + "https://www.legislation.gov.uk/ukpga/2007/3/section/37", + "https://www.legislation.gov.uk/ukpga/2014/26/section/2", + "https://www.legislation.gov.uk/ukpga/2015/11/section/5", + "https://www.legislation.gov.uk/ukpga/1978/30/section/17", + "https://www.legislation.gov.uk/ukpga/1978/30/section/23", + ) + + 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. + 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], + ) + # "less only the personal relief": no blind person's or marriage + # allowance, and no married couple's allowance. + notional_tax = basic_rate * max_( + 0, profit - person("personal_allowance", period) + ) + class_4 = parameters(period).gov.hmrc.national_insurance.class_4 + lower = class_4.contribution_scale.components[1].threshold + upper = class_4.contribution_scale.components[2].threshold + notional_class_4 = where( + person("ni_class_4_liable", period), + class_4.contribution_scale.components[1].rate + * min_(max_(0, profit - lower), upper - lower), + 0, + ) + # Reg 38(2)(a) and reg 37(2)(a) as they stood on 5 April 2024: the + # Class 2 contributions payable, unless chargeable income does not + # exceed the threshold of SSCBA 1992 s. 11. + class_2 = parameters(period).gov.hmrc.national_insurance.class_2 + notional_class_2 = ( + (period.start.year < 2024) + * 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/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.py new file mode 100644 index 0000000000..8b8bcb3686 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tax_deduction.py @@ -0,0 +1,178 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit_earnings_income_tax import ( + DECISION, + PRE_CHARGE_TAX, +) +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + applicant_has_universal_credit, +) +from policyengine_uk.variables.household.demographic.country import Country + + +class council_tax_reduction_tax_deduction(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction deduction for income tax and National Insurance" + documentation = ( + "Income tax and National Insurance that Council Tax Reduction income " + "deducts, for the applicant and partner " + "(is_council_tax_reduction_applicant_or_partner). T is their income " + "tax before the winter fuel payment charge, and T(X) the same with " + "every person's X set to nil (pension_credit_income_tax_without_" + "employment_income, pension_credit_income_tax_without_earnings and " + "council_tax_reduction_income_tax_on_other_counted_income). E is " + "employed earnings: employment income and statutory sick, maternity " + "and paternity pay. S is self-employment income. U is the taxed " + "income the pensioner schemes do not count, such as interest, " + "dividends and rent from property. " + "Scotland's working-age scheme (SSI 2021/249) deducts tax from " + "earnings only. Where the applicant has no award of Universal Credit " + "(council_tax_reduction_universal_credit_award, which includes an " + "award the benefit cap or deductions reduce to nil), the formula " + "deducts T less T(E), never negative, primary Class 1 " + "contributions, and the notional tax and contributions on profit " + "(council_tax_reduction_scotland_self_employment_deductions; reg " + "51(9)). With an award it deducts T less T(E, S), never " + "negative, and the Class 1, 2 and 4 contributions charged (regs " + "49(6)(b) and 51(2) step 3). The pensioner schemes of England (SI " + "2012/2885), Wales (WSI 2013/3029) and Scotland (SSI 2012/319) " + "disregard the tax deducted from earnings and the tax payable on " + "income taken into account, so the formula deducts T less T(E), " + "never negative; T(E, S, U), the tax on the other counted income " + "with the uncounted income above it and without the Step 7 charges in " + "income_tax_charges (through the High Income Child Benefit Charge, " + "HMRC recovers Child Benefit, which these schemes do not count, and " + "the pension annual allowance charge falls on contributions, so the " + "formula treats neither as tax payable on income taken into " + "account); the notional tax and " + "contributions on profit (pension_credit_self_employment_deductions " + "in England and Wales, the Scottish variable in Scotland); and " + "primary Class 1 contributions. Earnings are the top slice of income " + "and self-employed profit sits beneath them, so the tax on earnings " + "includes any tax they add on savings and dividends and through the " + "marriage allowance. Benefits in kind stay in both terms of each " + "difference, since no scheme counts a payment in kind as earnings. " + "In the English and Welsh working-age schemes, and for families " + "outside Great Britain, the formula deducts what the legacy means " + "tests deduct: the applicant's and partner's " + "legacy_means_test_deducted_income_tax, with their sum floored at " + "nil, and all their National Insurance. " + DECISION + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ssi/2021/249/regulation/49", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/50", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/51", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/31", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/37", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/17", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/19", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/30", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/11", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/24", + ] + + def formula(benunit, period, parameters): + person = benunit.members + members = person("is_council_tax_reduction_applicant_or_partner", period) + + def total(values): + return benunit.sum(values * members) + + country = benunit.household("country", period) + scotland = country == Country.SCOTLAND + in_great_britain = ( + (country == Country.ENGLAND) | scotland | (country == Country.WALES) + ) + pensioner = benunit("council_tax_reduction_pensioner", period) + pensioner_scheme = in_great_britain & pensioner + scottish_working_age = scotland & ~pensioner + with_universal_credit = scottish_working_age & applicant_has_universal_credit( + benunit, period + ) + without_universal_credit = scottish_working_age & ~with_universal_credit + + income_tax = person(PRE_CHARGE_TAX, period) + tax = total(income_tax) + class_1 = total(person("ni_class_1_employee", period)) + zeros = np.zeros_like(tax) + + # The other schemes deduct what the legacy means tests deduct: the + # applicant's and partner's legacy_means_test_deducted_income_tax, + # whose sum the formula floors at nil because one member's value can + # turn negative where another's earnings move a Step 7 charge off + # them, and all their National Insurance. + other_schemes = ~( + with_universal_credit | without_universal_credit | pensioner_scheme + ) + if other_schemes.any(): + legacy_deduction = max_( + 0, total(person("legacy_means_test_deducted_income_tax", period)) + ) + total(person("national_insurance", period)) + else: + legacy_deduction = zeros + + # Each branch of income tax is read only where a scheme uses it. + if (without_universal_credit | pensioner_scheme).any(): + tax_without_employment = total( + person("pension_credit_income_tax_without_employment_income", period) + ) + else: + tax_without_employment = tax + tax_on_employed_earnings = max_(0, tax - tax_without_employment) + + if with_universal_credit.any(): + tax_without_earnings = total( + person("pension_credit_income_tax_without_earnings", period) + ) + else: + tax_without_earnings = tax + uc_deduction = ( + max_(0, tax - tax_without_earnings) + + class_1 + + total(add(person, period, ["ni_class_2", "ni_class_4"])) + ) + + scottish_notional = ( + total( + person( + "council_tax_reduction_scotland_self_employment_deductions", + period, + ) + ) + if (scotland & (without_universal_credit | pensioner_scheme)).any() + else zeros + ) + no_uc_deduction = tax_on_employed_earnings + class_1 + scottish_notional + + if pensioner_scheme.any(): + tax_on_other_counted_income = total( + person( + "council_tax_reduction_income_tax_on_other_counted_income", + period, + ) + ) + # The formula prices the English and Welsh notional tax with + # Pension Credit's variable, because the Secretary of State and + # the Welsh Ministers allow the same personal reliefs (SI + # 2012/2885 Sch 1 para 30; WSI 2013/3029 Sch 1 para 24): those + # under ITA 2007 Part 3 Chapters 2, 3 and 3A. + england_and_wales_notional = total( + person("pension_credit_self_employment_deductions", period) + ) + else: + tax_on_other_counted_income = zeros + england_and_wales_notional = zeros + notional = where(scotland, scottish_notional, england_and_wales_notional) + pensioner_deduction = ( + tax_on_employed_earnings + tax_on_other_counted_income + notional + class_1 + ) + + return select( + [with_universal_credit, without_universal_credit, pensioner_scheme], + [uc_deduction, no_uc_deduction, pensioner_deduction], + default=legacy_deduction, + )