diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md new file mode 100644 index 0000000000..84d4dc45bf --- /dev/null +++ b/changelog.d/2350.fixed.md @@ -0,0 +1 @@ +The model now reports Scottish tax bands from 2017-18 using Scottish limits for non-savings, non-dividend income and UK limits for savings and dividends, and tests the Marriage Allowance income conditions directly under ITA 2007 s. 55B(2)(b) and (ba), allowing income at a rate limit and savings at a permitted nil rate. It gives the Marriage Allowance recipient a Step 6 tax reduction of the basic rate, or the Scottish basic rate, on the transferable amount (s. 55B(1) and (3)), in place of a deduction from taxable income. A reduction larger than the tax on earnings now also comes off the tax the Welsh working-age Council Tax Reduction route disregards on pensions and benefits. diff --git a/docs/engineering/gift-aid.md b/docs/engineering/gift-aid.md index 6303e0ba16..feb5968b63 100644 --- a/docs/engineering/gift-aid.md +++ b/docs/engineering/gift-aid.md @@ -84,8 +84,9 @@ order, each against the extended limits. Property income is taxed on top of and savings and dividends on top of `non_savings_taxable_income` (`earned_taxable_income` plus property income after allowances). Both bases derive from `net_income`, so the gift raises the limits without also shrinking -the income beneath them. They differ only when Marriage Allowance applies, -which `earned_taxable_income` subtracts (#1947). The Personal Savings Allowance +the income beneath them. The two bases come to the same amount; the Marriage +Allowance reduces neither, since the model gives it as a tax reduction at Step 6 +(`marriage_allowance_tax_reduction`). The Personal Savings Allowance compares net income less allowances with the UK basic and higher rate limits, both extended by the gross gift ([s. 12B](https://www.legislation.gov.uk/ukpga/2007/3/section/12B), including s. 12B(8) for Scottish taxpayers, with s. 414(2)(b)). diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml deleted file mode 100644 index 526252c4ed..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: Tax bands whose members may transfer personal allowance under the Marriage Allowance election (ITA 2007 s.55B). A transferor is eligible if they are not liable at the higher rate or above; the Scottish intermediate rate is treated as within the basic-rate envelope for this purpose. -values: - 0000-01-01: - - NONE - - STARTER - - BASIC - - INTERMEDIATE -metadata: - unit: list - label: Marriage Allowance eligible tax bands - period: year - reference: - - title: "Income Tax Act 2007, s. 55B" - href: https://www.legislation.gov.uk/ukpga/2007/3/section/55B diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml index bbde24b1b1..a63a25a863 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml @@ -1,4 +1,4 @@ -description: Maximum Marriage Allowance taxable income reduction, as a percentage of the full Personal Allowance. +description: Maximum Marriage Allowance transferable amount, as a percentage of the full Personal Allowance. The recipient gets a tax reduction of the appropriate percentage of that amount. values: 2016-04-01: 0.1 metadata: diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml index d40254f49b..f4e83a3d2c 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml @@ -5,7 +5,6 @@ values: - taxable_dividend_income - taxable_property_income - received_allowances_earned_income - - marriage_allowance metadata: unit: list diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/step_6_tax_reductions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/step_6_tax_reductions.yaml index 8e27c45473..f3fa7fad4f 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/step_6_tax_reductions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/step_6_tax_reductions.yaml @@ -10,6 +10,10 @@ metadata: href: https://www.legislation.gov.uk/ukpga/2007/3/section/26 - title: Income Tax Act 2007 s. 29(2) and (3) href: https://www.legislation.gov.uk/ukpga/2007/3/section/29 + - title: Income Tax Act 2007 s. 27(2) + href: https://www.legislation.gov.uk/ukpga/2007/3/section/27 + - title: Income Tax Act 2007 s. 55A(2) + href: https://www.legislation.gov.uk/ukpga/2007/3/section/55A - title: Budget 2026, paragraph 6.182 (Pension Tax Allowance) href: https://assets.publishing.service.gov.uk/media/6ae1ec57bdb574cf1c9b3f12/E03668986_HMT_Budget_2026_ELAY__2_.pdf#page=135 values: @@ -19,3 +23,9 @@ values: - property_finance_cost_relief # Budget 2026, 2027-28 to 2029-30; zero in other years. - pension_tax_allowance + # ITA 2007 s. 55A(2) and s. 55B(1). Section 27(5) does not list Chapter 3A, + # so s. 27(2) sets its place: the order that gives the greatest reduction. + # Each reduction above has an amount fixed before Step 6, so every order + # deducts the same total. Listing this one last leaves the amounts of the + # reductions above as they were. + - marriage_allowance_tax_reduction diff --git a/policyengine_uk/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index 16ddaa7960..2dd2427fa5 100644 --- a/policyengine_uk/reforms/cps/marriage_tax_reforms.py +++ b/policyengine_uk/reforms/cps/marriage_tax_reforms.py @@ -1,6 +1,9 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.income_tax import net_income_from_adjusted_net_income from policyengine_uk.utils.supplied_inputs import supplied_input +from policyengine_uk.variables.gov.hmrc.income_tax.allowances.meets_marriage_allowance_income_conditions import ( + liable_only_at_marriage_allowance_rates, +) from typing import Union, Optional @@ -34,23 +37,22 @@ def formula(person, period): class meets_marriage_allowance_income_conditions(Variable): label = "Meets Marriage Allowance income conditions" - documentation = "Whether this person (and their partner) meets the conditions for this person to be eligible for the Marriage Allowance, as set out in the Income Tax Act 2007 sections 55B and 55C" + documentation = ( + "Whether this person meets the statutory Marriage Allowance income " + "conditions or this reform's expansion conditions." + ) entity = Person definition_period = YEAR value_type = bool reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B" def formula(person, period, parameters): - band = person("tax_band", period) - eligible_bands = parameters( - period - ).gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands - base_eligible = np.isin(band.decode_to_str(), eligible_bands) - # Expand to higher bands if the reform's expansion conditions are met. - bands = band.possible_values - expansion = person("meets_expanded_ma_conditions", period) & ( - (band == bands.HIGHER) | (band == bands.ADDITIONAL) + base_eligible = liable_only_at_marriage_allowance_rates( + person, period, parameters ) + # Expand to people liable above those rates who meet the reform's + # expansion conditions. + expansion = person("meets_expanded_ma_conditions", period) & ~base_eligible return base_eligible | expansion class marriage_allowance(Variable): diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml index e753affdcf..5ba8c49947 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml @@ -411,7 +411,6 @@ - taxable_dividend_income - taxable_property_income - received_allowances_earned_income - - marriage_allowance - taxable_pension_income age: 40 employment_income: 20_000 diff --git a/policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rates.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rates.yaml index 973b2c42b2..75b461d549 100644 --- a/policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rates.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rates.yaml @@ -127,3 +127,572 @@ earned_taxable_income: 130000 earned_income_tax: 50034.35 income_tax: 50034.35 + +# tax_band for a Scottish taxpayer with savings or dividends (#2350). +# Parliament charges the Scottish rates only on income that is neither +# savings nor dividends (ITA 2007 s. 11A(1A), (4) and (5), and s. 13; +# S6M-16531 for 2025-26). +# Savings and dividends form the top slice (s. 16) and take the rates and +# bands they would bear outside Scotland (ss. 11D(6) and 13(5)). The model +# returns the higher band. In 2025-26 Parliament sets the personal allowance +# at £12,570, the basic rate limit at £37,700, the dividend allowance at £500 +# (8.75% ordinary, 33.75% upper) and the Personal Savings Allowance at +# £1,000, or £500 with higher-rate income (s. 12B). The Scottish Parliament +# sets the Scottish limits on taxable income at £2,827, £14,921, £31,092, +# £62,430 and £125,140 (19%, 20%, 21%, 42%, 45%, 48%). The Marriage +# Allowance test applies the rate conditions of s. 55B(2)(b) and the +# dividend counterfactual of s. 55B(2)(ba). Savings at the savings nil rate +# do not bar the Marriage Allowance. + +- name: Scottish 2025 pay of 40000 and dividends of 5000 reaches the intermediate band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 40_000 + dividend_income: 5_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 27,430 taxable, intermediate. 2,827 x 19% + 12,094 x 20% + # + 12,509 x 21% = 537.13 + 2,418.80 + 2,626.89 = 5,582.82. + # Dividends: 27,430 to 32,430, below 37,700: (5,000 - 500) x 8.75%. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 5_582.82 + dividend_income_tax: 393.75 + +- name: Scottish 2025 pay of 40000 and savings of 5000 reaches the intermediate band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 40_000 + savings_interest_income: 5_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay as above. Savings: 27,430 to 32,430, below 37,700, so the £1,000 + # allowance and (5,000 - 1,000) x 20% = 800. The model assigns no savings + # to the starting rate because other taxable income exceeds £5,000 + # (s. 12). + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 5_582.82 + savings_income_tax: 800 + +- name: England 2025 pay of 40000 and dividends of 5000 reaches the basic band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 40_000 + dividend_income: 5_000 + households: + household: + members: [person] + country: ENGLAND + output: + # 32,430 of taxable income, below 37,700. 27,430 x 20% = 5,486. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true + earned_income_tax: 5_486 + dividend_income_tax: 393.75 + +- name: Scottish 2025 pay of 60000 and dividends of 20000 reaches the higher band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 60_000 + dividend_income: 20_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 47,430 taxable, higher, not advanced. 537.13 + 2,418.80 + # + 16,171 x 21% + 16,338 x 42% = 537.13 + 2,418.80 + 3,395.91 + # + 6,861.96 = 13,213.80. Dividends: 47,430 to 67,430, all above + # 37,700 and below 125,140: (20,000 - 500) x 33.75% = 6,581.25. No + # income reaches the advanced, top or dividend additional rate. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 13_213.80 + dividend_income_tax: 6_581.25 + +- name: Scottish 2025 pay of 60000 and savings of 20000 reaches the higher band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 60_000 + savings_interest_income: 20_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay as above. Savings: 47,430 to 67,430, all at the higher rate with + # a £500 allowance: 19,500 x 40% = 7,800. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 13_213.80 + savings_income_tax: 7_800 + +- name: England 2025 pay of 60000 and dividends of 20000 reaches the higher band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 60_000 + dividend_income: 20_000 + households: + household: + members: [person] + country: ENGLAND + output: + # 37,700 x 20% + 9,730 x 40% = 7,540 + 3,892 = 11,432. + tax_band: HIGHER + earned_income_tax: 11_432 + dividend_income_tax: 6_581.25 + +- name: Scottish 2025 pay of 30000 and dividends of 25000 stays higher + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 30_000 + dividend_income: 25_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 17,430 taxable, intermediate. 537.13 + 2,418.80 + 2,509 x 21% + # = 3,482.82. Dividends: 17,430 to 42,430. 20,270 below 37,700, the + # first 500 at nil: 19,770 x 8.75% = 1,729.875; 4,730 above it at the + # upper rate: 4,730 x 33.75% = 1,596.375. Total 3,326.25. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 3_482.82 + dividend_income_tax: 3_326.25 + +- name: Scottish 2025 pay of 30000 and savings of 25000 stays higher + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 30_000 + savings_interest_income: 25_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Savings: 17,430 to 42,430 with a £500 allowance. 19,770 x 20% + # + 4,730 x 40% = 3,954 + 1,892 = 5,846. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 3_482.82 + savings_income_tax: 5_846 + +- name: Scottish 2025 income at both limits stays intermediate + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 43_662 + dividend_income: 6_608 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 31,092 taxable, exactly the intermediate rate's limit, so none + # at 42%: 537.13 + 2,418.80 + 16,171 x 21% = 6,351.84. Dividends: + # 31,092 to 37,700, exactly the basic rate limit, so all at the + # ordinary rate: (6,608 - 500) x 8.75% = 534.45. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 6_351.84 + dividend_income_tax: 534.45 + +- name: Scottish 2025 dividends one pound above the basic rate limit reach the higher band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 43_662 + dividend_income: 6_609 + households: + household: + members: [person] + country: SCOTLAND + output: + # As above, with £1 of dividends above 37,700 at the upper rate: + # 534.45 + 1 x 33.75% = 534.7875. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 6_351.84 + dividend_income_tax: 534.79 + +- name: Scottish 2025 Gift Aid raises the Scottish and basic rate limits + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 44_462 + dividend_income: 5_000 + gift_aid: 800 + households: + household: + members: [person] + country: SCOTLAND + output: + # The gift grosses up to 1,000, which raises the Scottish limits from + # the basic rate's upper limit and the basic rate limit (s. 414(2)(b)). + # Pay: 31,892 taxable, below 31,092 + 1,000. 537.13 + 13,094 x 20% + # + 15,971 x 21% = 537.13 + 2,618.80 + 3,353.91 = 6,509.84. + # Dividends: 31,892 to 36,892, below 38,700: 4,500 x 8.75% = 393.75. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 6_509.84 + dividend_income_tax: 393.75 + +- name: Scottish 2025 allowance beyond pay goes to dividends, which reach the basic band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 5_000 + dividend_income: 30_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # The allowance takes all 5,000 of pay first, then 7,570 of dividends, + # leaving 22,430 of dividends below 37,700 and nothing on the Scottish + # rates: (22,430 - 500) x 8.75% = 1,918.875. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true + earned_income_tax: 0 + dividend_income_tax: 1_918.88 + +- name: Scottish 2025 allowance beyond pay goes to savings, which reach the basic band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 5_000 + savings_interest_income: 30_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # 22,430 of savings remain, with no other taxable income: 5,000 at the + # starting rate for savings (s. 12), 1,000 of allowance, then + # 16,430 x 20% = 3,286. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true + earned_income_tax: 0 + savings_income_tax: 3_286 + +- name: Scottish 2025 starter-rate pay without savings or dividends reaches the starter band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 14_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # 1,430 taxable, all at 19% = 271.70. The person has no savings or + # dividends, so the basic rate limit does not apply. + tax_band: STARTER + earned_income_tax: 271.70 + +- name: Scottish 2025 pay equal to the personal allowance has no band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_570 + households: + household: + members: [person] + country: SCOTLAND + output: + tax_band: NONE + earned_income_tax: 0 + +- name: Scottish 2018-19 pay of 40000 and dividends of 5000 reaches the intermediate band + period: 2018 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 40_000 + dividend_income: 5_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # gov.scot, Scottish income tax 2018-2019: starter to £13,850, basic to + # £24,000, intermediate to £43,430 of income with an £11,850 allowance. + # Pay: 28,150 taxable, intermediate. 2,000 x 19% + 10,150 x 20% + # + 16,000 x 21% = 380 + 2,030 + 3,360 = 5,770. Dividends: 28,150 to + # 33,150, below the 34,500 basic rate limit, with a £2,000 allowance: + # 3,000 x 7.5% = 225. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 5_770 + dividend_income_tax: 225 + +- name: Scottish 2017-18 pay above 43000 reaches the higher band + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 44_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # gov.scot, Income tax rates set (2017): the Scottish higher rate starts + # at £43,000 of income, £31,500 above the £11,500 allowance. 32,500 + # taxable: 31,500 x 20% + 1,000 x 40% = 6,700. In 2017-18 Parliament + # allows the Scottish basic rate but not the Scottish higher rate + # (s. 55B(2)(b)). + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 6_700 + +# The same rules in 2026-27, with the Scottish limits of S6M-20844: £3,967, +# £16,956, £31,092, £62,430 and £125,140 of taxable income. The personal +# allowance stays £12,570 and the basic rate limit £37,700. + +- name: Scottish 2026 pay of 40000 and dividends of 5000 reaches the intermediate band + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 40_000 + dividend_income: 5_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 27,430 taxable, intermediate. 3,967 x 19% + 12,989 x 20% + # + 10,474 x 21% = 753.73 + 2,597.80 + 2,199.54 = 5,551.07. + # Dividends: 27,430 to 32,430, below 37,700. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + earned_income_tax: 5_551.07 + +- name: Scottish 2026 pay of 60000 and savings of 20000 reaches the higher band + period: 2026 + input: + people: + person: + age: 40 + employment_income: 60_000 + savings_interest_income: 20_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 47,430 taxable, higher, below 62,430. Savings: 47,430 to 67,430, + # above 37,700 and below 125,140. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + +- name: Scottish 2026 income at both limits stays intermediate + period: 2026 + input: + people: + person: + age: 40 + employment_income: 43_662 + dividend_income: 6_608 + households: + household: + members: [person] + country: SCOTLAND + output: + # Pay: 31,092 taxable, the intermediate rate's limit. Dividends: 31,092 + # to 37,700, the basic rate limit. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + +- name: Scottish 2026 dividends one pound above the basic rate limit reach the higher band + period: 2026 + input: + people: + person: + age: 40 + employment_income: 43_662 + dividend_income: 6_609 + households: + household: + members: [person] + country: SCOTLAND + output: + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + +- name: Scottish 2026 allowance beyond pay goes to dividends, which reach the basic band + period: 2026 + input: + people: + person: + age: 40 + employment_income: 5_000 + dividend_income: 30_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # After allowances, the taxpayer has 22,430 of taxable dividends below + # 37,700 and pays no tax at Scottish rates. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true + +# Boundaries the cases above leave open, 2025-26. + +- name: Scottish 2025 dividends at the Gift Aid limit stay basic + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + dividend_income: 50_271 + gift_aid: 0.8 + households: + household: + members: [person] + country: SCOTLAND + output: + # The 80p gift grosses up to 1 (s. 415) and raises the basic rate limit + # to 37,701 (s. 414(2)(b)). The allowance takes 12,570 of the dividends, + # which leaves 37,701, all within that limit: + # (37,701 - 500) x 8.75% = 3,255.0875. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true + dividend_income_tax: 3_255.09 + +- name: Scottish 2025 savings at the higher rate limit stay higher + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + savings_interest_income: 125_140 + households: + household: + members: [person] + country: SCOTLAND + output: + # At income of 125,140 Parliament withdraws the whole personal allowance + # (s. 35), so the person pays tax on all 125,140, up to the 125,140 + # higher rate limit and not above it. With no other income, 5,000 bears + # the starting rate for savings (s. 12) and the higher-rate allowance + # covers 500 (s. 12B); the rest of the 37,700 bears 20% and the 87,440 + # above it 40%: + # 32,200 x 20% + 87,440 x 40% = 6,440 + 34,976 = 41,416. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + savings_income_tax: 41_416 + +- name: Scottish 2025 property income at the higher rate limit stays intermediate + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_570 + property_income: 31_092 + households: + household: + members: [person] + country: SCOTLAND + output: + # The model reads property_income as profit and, with no receipts given, + # takes a profit above the 1,000 property allowance to reflect the better + # of expenses and the allowance already, so it taxes all 31,092. Pay + # takes the personal allowance. Property income bears the Scottish rates + # (s. 11A): 2,827 x 19% + 12,094 x 20% + 16,171 x 21% = 537.13 + # + 2,418.80 + 3,395.91 = 6,351.84, with none above 31,092. + tax_band: INTERMEDIATE + meets_marriage_allowance_income_conditions: true + property_income_tax: 6_351.84 + +- name: Scottish 2025 property income one pound above the higher rate limit reaches the higher band + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_570 + property_income: 31_093 + households: + household: + members: [person] + country: SCOTLAND + output: + # As above with 1 at the Scottish higher rate: 6,351.84 + 0.42. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + property_income_tax: 6_352.26 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml index 2131ed247c..8450543a61 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -337,12 +337,25 @@ members: [claimant, partner] country: SCOTLAND output: - # The 1,040 of earnings add 210.89 of income tax (they cross into the - # Scottish intermediate band), so net earnings are 829.11 and the 20 pound - # disregard of 1,040 is capped there; income is as with no earnings (next). - income_tax: [3_016.62, 0] - pension_credit_earnings_disregard: 829.11 - pension_credit_income: 25_194.27 + # 2025-26 Scottish schedule on taxable income: 19% up to 2,827, 20% to + # 14,921, 21% to 31,092. The partner's unused personal allowance gives the + # claimant a Marriage Allowance of 1,260 (10% of 12,570, rounded up to + # 10), which comes off as a tax reduction of 20% x 1,260 = 252, the + # Scottish basic rate (ITA 2007 s. 55B(1) and (3)). + # With earnings: 12,000 + 16,000 + 1,040 - 12,570 = 16,470 taxable; + # 2,827 x 19% + 12,094 x 20% + 1,549 x 21% = 537.13 + 2,418.80 + 325.29 + # = 3,281.22, less 252 = 3,029.22. + # Without earnings (next case): 15,430 taxable; 537.13 + 2,418.80 + + # 509 x 21% = 3,062.82, less 252 = 2,810.82. + # The taxable income exceeds 14,921 even without the earnings, so all + # 1,040 of the earnings bear 21%, adding 3,029.22 - 2,810.82 = 218.40 of + # tax. The claimant pays no Class 1 contributions above State Pension age, + # so net earnings come to 1,040 - 218.40 = 821.60, and the 20 pound + # disregard (attendance allowance), up to 1,040 a year, covers all 821.60. + # Income: 29,040 - 3,029.22 - 821.60 = 25,189.18, as with no earnings. + income_tax: [3_029.22, 0] + pension_credit_earnings_disregard: 821.60 + pension_credit_income: 25_189.18 - name: The same Scottish couple with no earnings has the same Pension Credit income. period: 2025 @@ -365,9 +378,13 @@ members: [claimant, partner] country: SCOTLAND output: - income_tax: [2_805.73, 0] + # 28,000 - 12,570 = 15,430 taxable: 2,827 x 19% + 12,094 x 20% + 509 x + # 21% = 3,062.82, less the 252 Marriage Allowance reduction (20% x 1,260) + # = 2,810.82. Income: 12,000 + 16,000 - 2,810.82 = 25,189.18; attendance + # allowance does not count. + income_tax: [2_810.82, 0] pension_credit_earnings_disregard: 0 - pension_credit_income: 25_194.27 + pension_credit_income: 25_189.18 - name: Caring hours without entitlement to carer's allowance do not qualify for the 20 pound disregard (Sch VI para 3 needs Sch I para 4). period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.yaml new file mode 100644 index 0000000000..fc82157caa --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.yaml @@ -0,0 +1,376 @@ +# The Marriage Allowance as a tax reduction at Step 6 (ITA 2007 s. 55A(2)): +# the appropriate percentage of the transferable amount (s. 55B(1)), the basic +# rate or, for a Scottish taxpayer, the Scottish basic rate (s. 55B(3)), +# deducted only so far as Step 5 tax remains after earlier reductions +# (s. 29(2) and (3)). +# +# Worked by hand from the parameter files. 2025-26: personal allowance +# £12,570, transferable amount 10% x 12,570 = 1,257 rounded up to £1,260 +# (s. 55B(4)(b) and (5)), basic rate 20% on the first £37,700, dividend +# allowance £500 and dividend ordinary rate 8.75%, Personal Savings Allowance +# £1,000 for a basic-rate taxpayer, starting rate limit for savings £5,000. +# Scottish rates for 2025-26 (S6M-16531): 19% up to £2,827, 20% to £14,921, +# 21% to £31,092, 42% above. Employee NI: 8% above £12,570. Each household +# also pays the £174.50 colour TV licence (gov.dcms.bbc.tv_licence.colour). + +- name: A Scottish intermediate-rate recipient with dividends gets 20% of the transferable amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 40_000 + dividend_income: 5_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + households: + household: + members: [earner, spouse] + region: SCOTLAND + output: + # The spouse's £5,000 leaves 12,570 - 5,000 = 7,570 of allowance unused, + # so the earner gets the full £1,260. + marriage_allowance: [1_260, 0] + marriage_allowance_appropriate_percentage: [0.2, 0.2] + # 20% x 1,260 = 252. + marriage_allowance_tax_reduction: [252, 0] + # Non-savings taxable income 40,000 - 12,570 = 27,430: + # 2,827 x 19% + (14,921 - 2,827) x 20% + (27,430 - 14,921) x 21% + # = 537.13 + 2,418.80 + 2,626.89 = 5,582.82. + # Dividends: 27,430 + 5,000 = 32,430 stays within £37,700 (ss. 11D(6) and + # 13(5)), so (5,000 - 500) x 8.75% = 393.75. Step 5: 5,976.57. + # Step 6: 5,976.57 - 252 = 5,724.57. + income_tax: [5_724.57, 0] + # 50,000 - 5,724.57 - (40,000 - 12,570) x 8% - 174.50 + # = 50,000 - 5,724.57 - 2,194.40 - 174.50 = 41,906.53. + household_net_income: 41_906.53 + +- name: A Scottish intermediate-rate recipient with savings gets 20% of the transferable amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 40_000 + savings_interest_income: 5_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + households: + household: + members: [earner, spouse] + region: SCOTLAND + output: + marriage_allowance: [1_260, 0] + marriage_allowance_tax_reduction: [252, 0] + # Non-savings tax 5,582.82 as above. Taxable income 32,430 stays within the + # rest-of-UK basic rate limit, so the Personal Savings Allowance is £1,000 + # (s. 12B); non-savings income of 27,430 leaves no starting-rate band. + # Savings: (5,000 - 1,000) x 20% = 800. Step 5: 6,382.82. + # Step 6: 6,382.82 - 252 = 6,130.82. + income_tax: [6_130.82, 0] + # 50,000 - 6,130.82 - 2,194.40 - 174.50 = 41,500.28. + household_net_income: 41_500.28 + +- name: A Scottish recipient with pay only gets 20% of the transferable amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 40_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + households: + household: + members: [earner, spouse] + region: SCOTLAND + output: + marriage_allowance_tax_reduction: [252, 0] + # 5,582.82 - 252 = 5,330.82. + income_tax: [5_330.82, 0] + # Universal Credit deducts the tax on earnings after the Step 6 + # reductions: 5,582.82 - 252 = 5,330.82. + uc_income_tax_on_earnings: [5_330.82, 0] + +- name: A Scottish starter-rate recipient gets 20% of the transferable amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 14_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + households: + household: + members: [earner, spouse] + region: SCOTLAND + output: + marriage_allowance_tax_reduction: [252, 0] + # (14,000 - 12,570) x 19% = 271.70; 271.70 - 252 = 19.70. + income_tax: [19.70, 0] + +- name: A recipient's starting rate for savings does not grow with the Marriage Allowance + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 17_570 + savings_interest_income: 3_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + marriage_allowance_tax_reduction: [252, 0] + # Non-savings taxable income 17,570 - 12,570 = 5,000 uses up the £5,000 + # starting rate limit for savings (s. 12), so no savings bear the starting + # rate. Pay: 5,000 x 20% = 1,000. Savings: (3,000 - 1,000) x 20% = 400. + # Step 5: 1,400. Step 6: 1,400 - 252 = 1,148. + income_tax: [1_148, 0] + +- name: A recipient whose tax comes only from dividends gets the full reduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 12_570 + dividend_income: 5_000 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + marriage_allowance: [1_260, 0] + marriage_allowance_tax_reduction: [252, 0] + # 2026-27: the personal allowance of £12,570 covers the pay. Dividends: + # (5,000 - 500) x 10.75% = 483.75. Step 6: 483.75 - 252 = 231.75. + income_tax: [231.75, 0] + +- name: A basic-rate recipient in England keeps the same tax under the reduction + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 30_000 + spouse: + age: 40 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + marriage_allowance_tax_reduction: [252, 0] + # (30,000 - 12,570) x 20% - 1,260 x 20% = 3,486 - 252 = 3,234, as when the + # model took 1,260 off the 20% band. + income_tax: [3_234, 0] + uc_income_tax_on_earnings: [3_234, 0] + +- name: The reduction stops at the recipient's Step 5 tax + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 13_500 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + marriage_allowance: [1_260, 0] + # Step 5: (13,500 - 12,570) x 20% = 186, less than 252 (s. 29(2)). + marriage_allowance_tax_reduction: [186, 0] + income_tax: [0, 0] + +- name: The reduction takes only the tax that earlier reductions leave + period: 2025 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 14_000 + other_tax_credits: 100 + spouse: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + # Step 5: (14,000 - 12,570) x 20% = 286. The earlier reductions take 100, + # leaving 186 (s. 29(3)), less than 252. + marriage_allowance_tax_reduction: [186, 0] + income_tax_reductions: [286, 0] + income_tax: [0, 0] + +# The appropriate percentage (s. 55B(3)) for a Scottish and an English +# taxpayer. The model's Scottish basic rate is 20% in each year; the Scottish +# rates of s. 11A start in 2016-17, so in 2015-16 both take the basic rate. + +- name: Appropriate percentage in 2015-16 + period: 2015 + input: + people: + scot: + age: 40 + english: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + english_household: + members: [english] + region: LONDON + output: + marriage_allowance_appropriate_percentage: [0.2, 0.2] + +- name: Appropriate percentage in 2017-18 + period: 2017 + input: + people: + scot: + age: 40 + english: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + english_household: + members: [english] + region: LONDON + output: + marriage_allowance_appropriate_percentage: [0.2, 0.2] + +- name: Appropriate percentage in 2018-19 + period: 2018 + input: + people: + scot: + age: 40 + english: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + english_household: + members: [english] + region: LONDON + output: + marriage_allowance_appropriate_percentage: [0.2, 0.2] + +- name: Appropriate percentage in 2025-26 + period: 2025 + input: + people: + scot: + age: 40 + english: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + english_household: + members: [english] + region: LONDON + output: + marriage_allowance_appropriate_percentage: [0.2, 0.2] + +- name: A reformed Scottish basic rate sets a Scottish taxpayer's appropriate percentage + period: 2025 + input: + gov.hmrc.income_tax.rates.scotland.rates[1].rate: 0.22 + people: + scot: + age: 40 + english: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + english_household: + members: [english] + region: LONDON + output: + marriage_allowance_appropriate_percentage: [0.22, 0.2] + +- name: Before 2016-17 a Scottish taxpayer's appropriate percentage stays the basic rate + period: 2015 + input: + gov.hmrc.income_tax.rates.scotland.rates[1].rate: 0.22 + people: + scot: + age: 40 + households: + scottish_household: + members: [scot] + region: SCOTLAND + output: + marriage_allowance_appropriate_percentage: 0.2 + +- name: A reformed basic rate sets the reduction outside Scotland + period: 2025 + absolute_error_margin: 0.01 + input: + gov.hmrc.income_tax.rates.uk[0].rate: 0.21 + people: + earner: + age: 40 + employment_income: 30_000 + spouse: + age: 40 + benunits: + benunit: + members: [earner, spouse] + is_married: true + output: + marriage_allowance_appropriate_percentage: [0.21, 0.21] + # 21% x 1,260 = 264.60. + marriage_allowance_tax_reduction: [264.60, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml index 064d9679fc..981bdc61be 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml @@ -1,41 +1,366 @@ -- name: Basic rate band is eligible for Marriage Allowance +# ITA 2007 s. 55B(2)(b) and (ba): the person receiving the allowance may be +# liable only at the basic rates, the Scottish starter and intermediate rates +# (from 2018-19, SI 2018/459 art. 6(2)), the dividend ordinary rate and the nil +# and starting rates for savings, and dividends count as if the dividend nil +# rate (s. 13A) did not exist. From 2021-22 to 2026-27 Parliament sets the +# personal allowance at £12,570 and the basic rate limit at £37,700, and the +# Scottish Parliament starts the Scottish higher rate at £31,092 of taxable +# income (S6M-16531 for 2025-26, S6M-20844 for 2026-27). In 2026-27 Parliament +# sets the dividend allowance at £500 and the Personal Savings Allowance at +# £1,000, or £500 with income above the basic rate limit (s. 12B). + +- name: Basic-rate earner meets the Marriage Allowance income conditions period: 2021 input: - tax_band: BASIC + # 30,000 - 12,570 = 17,430, all at the basic rate. + employment_income: 30_000 + region: NORTH_EAST output: meets_marriage_allowance_income_conditions: true -- name: Higher rate band is ineligible for Marriage Allowance +- name: Higher-rate earner fails the Marriage Allowance income conditions period: 2021 input: - tax_band: HIGHER + # 60,000 - 12,570 = 47,430. The model taxes 9,730 at the higher rate. + employment_income: 60_000 + region: NORTH_EAST output: meets_marriage_allowance_income_conditions: false -- name: Additional rate band is ineligible for Marriage Allowance +- name: Additional-rate earner fails the Marriage Allowance income conditions period: 2021 input: - tax_band: ADDITIONAL + # The personal allowance falls to zero at 125,140 of income, and 10,000 + # of the income exceeds the 150,000 higher rate limit. + employment_income: 160_000 + region: NORTH_EAST output: meets_marriage_allowance_income_conditions: false -- name: Below-PA earner (band NONE) is eligible for Marriage Allowance (regression for #395) +- name: Earner below the personal allowance meets the conditions (regression for #395) period: 2021 input: - tax_band: NONE + employment_income: 10_000 + region: NORTH_EAST output: meets_marriage_allowance_income_conditions: true -- name: Scottish starter rate band is eligible for Marriage Allowance +- name: Scottish starter-rate earner meets the Marriage Allowance income conditions period: 2021 input: - tax_band: STARTER + # 430 of taxable income, all at the Scottish starter rate. + employment_income: 13_000 + region: SCOTLAND output: meets_marriage_allowance_income_conditions: true -- name: Scottish intermediate rate band is eligible for Marriage Allowance +- name: Scottish intermediate-rate earner meets the Marriage Allowance income conditions period: 2021 input: - tax_band: INTERMEDIATE + # 27,430 of taxable income, below the Scottish higher rate at 31,092. + employment_income: 40_000 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: Under a raised Scottish higher rate, nil-rate savings meet the conditions + period: 2025 + input: + # The Scottish higher rate moves to 37,702 of taxable income. Pay leaves + # 50,270 - 12,570 = 37,700, all below it. The 1 of interest takes taxable + # income to 37,701, above the basic rate limit, so Parliament sets the + # Personal Savings Allowance at 500 (s. 12B) and charges the pound at the + # savings nil rate (s. 12A), which it allows for the Marriage Allowance + # (s. 55B(2)(b)). The model bands the person HIGHER on that pound alone. + gov.hmrc.income_tax.rates.scotland.rates[3].threshold: 37_702 + employment_income: 50_270 + savings_interest_income: 1 + region: SCOTLAND + output: + tax_band: HIGHER + savings_income_tax: 0 + meets_marriage_allowance_income_conditions: true + +- name: Savings covered by the savings nil rate above the basic rate limit meet the conditions + period: 2026 + input: + # 50,070 - 12,570 = 37,500 of pay, then 500 of interest to 38,000. Income + # above the basic rate limit sets the Personal Savings Allowance at 500 + # (s. 12B), which covers all 500 at the savings nil rate (s. 12A). + employment_income: 50_070 + savings_interest_income: 500 + region: NORTH_EAST + output: + tax_band: HIGHER + savings_income_tax: 0 + meets_marriage_allowance_income_conditions: true + +- name: One pound of savings at the savings higher rate fails the conditions + period: 2026 + input: + # 50,270 - 12,570 = 37,700 of pay, at the basic rate limit, then 501 of + # interest. The higher-rate allowance covers 500 at the savings nil rate + # (s. 12A) and the last pound bears the savings higher rate, 40p. With pay + # at the limit, no other share of the allowance lowers the tax (s. 25(2)). + employment_income: 50_270 + savings_interest_income: 501 + region: NORTH_EAST + absolute_error_margin: 0.01 + output: + savings_income_tax: 0.4 + meets_marriage_allowance_income_conditions: false + +- name: Savings at a raised starting rate limit and the savings nil rate meet the conditions + period: 2026 + input: + # A reform raises the starting rate limit for savings to 37,500. Pay uses + # the personal allowance. Parliament charges 37,500 of interest at the + # starting rate (s. 12) and the 500 above that limit at the savings nil + # rate (s. 12A), and permits both for the Marriage Allowance + # (s. 55B(2)(b)). + gov.hmrc.income_tax.rates.savings_starter_rate.allowance: 37_500 + employment_income: 12_570 + savings_interest_income: 38_000 + region: NORTH_EAST + output: + savings_starter_rate_income: 37_500 + savings_allowance: 500 + savings_income_tax: 0 + meets_marriage_allowance_income_conditions: true + +- name: Under a starting rate limit above the basic rate limit, savings past the basic rate limit fail the conditions + period: 2026 + input: + # A reform raises the starting rate limit for savings to 40,000. Pay uses + # the personal allowance. Parliament charges the starting rate only on + # income that would otherwise bear the basic rate (s. 12(1)(b)), so it + # covers 37,700 of the 38,000 of interest, and the savings nil rate only on + # savings above the 40,000 limit (s. 12A). The interest stops below that + # limit, so the other 300 bears the savings higher rate, which Parliament + # forbids for the Marriage Allowance (s. 55B(2)(b)). The model's savings + # liability charges these 300 at the savings nil rate. + gov.hmrc.income_tax.rates.savings_starter_rate.allowance: 40_000 + employment_income: 12_570 + savings_interest_income: 38_000 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: Taxable income at the basic rate limit meets the conditions + period: 2026 + input: + # 50,270 - 12,570 = 37,700: Parliament charges income up to the basic rate + # limit at the basic rate (s. 10(2)). + employment_income: 50_270 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: true + +- name: One pound above the basic rate limit fails the conditions + period: 2026 + input: + # 37,701 of taxable income: 1 at the higher rate (s. 10(3)). + employment_income: 50_271 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: Scottish taxable income at the start of the higher rate meets the conditions + period: 2026 + input: + # 43,662 - 12,570 = 31,092: the Scottish Parliament charges the + # intermediate rate on income up to 31,092 and the higher rate only above + # it (S6M-20844). + employment_income: 43_662 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: One pound into the Scottish higher rate fails the conditions + period: 2026 + input: + employment_income: 43_663 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: false + +- name: Dividends covered by the dividend allowance above the basic rate limit fail the conditions + period: 2026 + input: + # 37,500 of pay, then 500 of dividends, all within the 500 dividend + # allowance (s. 13A). Without s. 13A, 300 of them would bear the dividend + # upper rate (37,500 + 500 = 38,000), so s. 55B(2)(ba) fails, though the + # person pays no dividend tax. + employment_income: 50_070 + dividend_income: 500 + region: NORTH_EAST + output: + dividend_income_tax: 0 + meets_marriage_allowance_income_conditions: false + +- name: Dividends that reach the basic rate limit exactly meet the conditions + period: 2026 + input: + # 37,500 + 200 = 37,700: none would bear the dividend upper rate. + employment_income: 50_070 + dividend_income: 200 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: true + +- name: Gift Aid raises the basic rate limit for the conditions + period: 2026 + input: + # A 1,600 gift grosses up to 2,000 (s. 415), which raises the basic rate + # limit to 39,700 (s. 414(2)(b)). 52,270 - 12,570 = 39,700. + employment_income: 52_270 + gift_aid: 1_600 + region: NORTH_EAST output: meets_marriage_allowance_income_conditions: true + +- name: One pound above the Gift Aid limit fails the conditions + period: 2026 + input: + employment_income: 52_271 + gift_aid: 1_600 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: Gift Aid raises the start of the Scottish higher rate for the conditions + period: 2026 + input: + # The 2,000 grossed-up gift moves the Scottish higher rate to 33,092 + # (s. 414(2)(b)). 45,662 - 12,570 = 33,092. + employment_income: 45_662 + gift_aid: 1_600 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: One pound above the Scottish Gift Aid limit fails the conditions + period: 2026 + input: + employment_income: 45_663 + gift_aid: 1_600 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: false + +- name: In 2015-16 the basic rate limit bounds a Scottish taxpayer's income + period: 2015 + input: + # The Scottish rates of s. 11A start in 2016-17 (SI 2015/2000 art. 3), and + # in 2015-16 Parliament allows the basic rate (s. 55B(2)(b)). + # 42,200 - 10,600 = 31,600, below the basic rate limit, though above the + # 31,500 the model reads from its 2017-18 Scottish scale. + employment_income: 42_200 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2016-17 the basic rate limit bounds a Scottish taxpayer's income + period: 2016 + input: + # The Treasury added the Scottish basic rate to s. 55B(2)(b) (SI 2015/1810 + # art. 14(3)), and in 2016-17 Parliament charges it on non-savings income + # that would otherwise bear the basic rate (s. 11A(1)). + # 42,700 - 11,000 = 31,700, below the basic rate limit. + employment_income: 42_700 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2017-18 the Scottish higher rate fails the conditions + period: 2017 + input: + # In 2017-18 Parliament allows only the Scottish basic rate (s. 55B(2)(b)). + # With an 11,500 allowance, the Scottish higher rate starts at 31,500 of + # taxable income: 43,001 - 11,500 = 31,501. + employment_income: 43_001 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: false + +- name: In 2017-18 Scottish income up to the higher rate meets the conditions + period: 2017 + input: + # 43,000 - 11,500 = 31,500, all at the Scottish basic rate. + employment_income: 43_000 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2017-18 the same income outside Scotland meets the conditions + period: 2017 + input: + # 31,501 of taxable income, below the 33,500 basic rate limit. + employment_income: 43_001 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2018-19 the Scottish intermediate rate meets the conditions + period: 2018 + input: + # The Treasury adds the Scottish starter and intermediate rates to + # s. 55B(2)(b) (SI 2018/459 art. 6(2)). 40,000 - 11,850 = 28,150, at the + # intermediate rate. + employment_income: 40_000 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2015-16 dividends above the basic rate limit fail the conditions + period: 2015 + input: + # 42,600 - 10,600 = 32,000 of pay, at the model's basic rate limit, then 1 + # of dividends. Parliament charges the dividend upper rate on dividends + # that would otherwise bear the higher rate (s. 13(2)), and the higher rate + # starts at the basic rate limit (s. 10(3)). In 2015-16 Parliament permits + # only the dividend ordinary rate among the dividend rates (s. 55B(2)(b)). + # The model's dividend scale starts the upper rate at 37,500 that year. + employment_income: 42_600 + dividend_income: 1 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: At the statutory 2015-16 basic rate limit a pound of dividends fails the conditions + period: 2015 + input: + # A reform sets the basic rate limit to the statutory 31,785 (s. 10(5) for + # 2015-16). 42,385 - 10,600 = 31,785 of pay, then 1 of dividends, which + # Parliament charges at the dividend upper rate (s. 13(2)). + gov.hmrc.income_tax.rates.uk[1].threshold: 31_785 + employment_income: 42_385 + dividend_income: 1 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: In 2015-16 savings above the basic rate limit fail the conditions + period: 2015 + input: + # 32,000 of pay, then 1 of interest. Parliament gave the savings nil rate + # effect from 2016-17 (FA 2016 s. 4(17)), so in 2015-16 the pound bears the + # higher rate, which it forbids for the Marriage Allowance (s. 55B(2)(b)). + # The model applies its Personal Savings Allowance in 2015-16 too. + employment_income: 42_600 + savings_interest_income: 1 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false + +- name: At the statutory 2015-16 basic rate limit a pound of savings fails the conditions + period: 2015 + input: + # As above with 1 of interest, which bears the higher rate in a year + # before the savings nil rate. + gov.hmrc.income_tax.rates.uk[1].threshold: 31_785 + employment_income: 42_385 + savings_interest_income: 1 + region: NORTH_EAST + output: + meets_marriage_allowance_income_conditions: false diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/property_income_band_base.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/property_income_band_base.yaml index 5a7f5ad0fd..40f4a75fde 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/property_income_band_base.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/property_income_band_base.yaml @@ -119,7 +119,7 @@ taxed_income: 47_430 personal_pension_contributions_tax: 4_000 -- name: Case 7, Marriage Allowance still comes off the band base (pinned; #1947). +- name: Case 7, Marriage Allowance leaves the band base unchanged. period: 2026 absolute_error_margin: 0.01 input: @@ -134,8 +134,10 @@ members: [person] country: ENGLAND output: - # earned_taxable_income subtracts Marriage Allowance, which the law gives - # as a tax reduction (ITA 2007 s. 55B), so the base is 30,000 - 12,570 - - # 1,260 = 16,170 rather than 17,430. Pinned here until #1947 is fixed. - non_savings_taxable_income: 16_170 + # Parliament gives the Marriage Allowance as a tax reduction at Step 6 + # (ITA 2007 ss. 55A(2) and 55B(1)), so it leaves the base at + # 30,000 - 12,570 = 17,430. + non_savings_taxable_income: 17_430 + # Step 5: 17,430 x 20% = 3,486. Step 6: 3,486 - 20% x 1,260 = 3,234. + income_tax: 3_234 non_savings_income_after_allowances: 17_430 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_devolved_working_age.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_devolved_working_age.yaml index 9005d13a41..8fa02cd473 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_devolved_working_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_devolved_working_age.yaml @@ -457,6 +457,43 @@ council_tax_reduction_working_age_applicable_income: 17_827 simulated_council_tax_reduction_benunit: 397.80 +- name: Wales, a Step 6 reduction larger than the tax on earnings comes off the tax on the pension + # Taxable income 13,000 + 3,000 - 12,570 = 3,430, so the tax before Step 6 is + # 686, and the lowest slice, the earnings, bears 430 x 20% = 86 of it. The + # partner's 1,260 of Marriage Allowance takes 252 off at Step 6, earnings + # first, so the tax on earnings is nil and the pension bears the other 434. + # Income: 13,000 - 34.40 Class 1 - 520 + 3,000 - 434 = 15,011.60; applicable + # amount 159.85 x 52 = 8,312.20; award 1,800 - 0.2 x 6,699.40 = 460.12. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 13_000 + private_pension_income: 3_000 + weekly_hours: 20 + council_tax_benefit_reported: 100 + partner: + age: 40 + benunits: + benunit: + members: [earner, partner] + would_claim_uc: false + households: + household: + members: [earner, partner] + region: WALES + country: WALES + council_tax: 1_800 + savings: 0 + output: + marriage_allowance: [1_260, 0] + uc_income_tax_on_earnings: [0, 0] + council_tax_reduction_working_age_unearned_income_tax: 434 + council_tax_reduction_working_age_applicable_income: 15_011.60 + simulated_council_tax_reduction_benunit: 460.12 + - name: Wales, the £17.10 needs the disabled member of a couple to be the one in work # Sch 8 para 18(2)(b)(v): the disabled claimant does not work, so only the # £20 disability disregard applies (para 4), 1,040 a year. diff --git a/policyengine_uk/tests/test_council_tax_reduction_devolved_working_age_properties.py b/policyengine_uk/tests/test_council_tax_reduction_devolved_working_age_properties.py index 6a28465c01..fb5982d1e4 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_devolved_working_age_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_devolved_working_age_properties.py @@ -33,6 +33,11 @@ disregard: the £17.10 additional disregard needs earnings that cover the charges, and from April 2022 only employed earnings qualify, so the disregard never changes for a family with no employed earnings. +7. For a family whose taxed income is pay and a private pension, the tax the + route takes on earnings (uc_income_tax_on_earnings) and the tax it takes on + counted unearned income (council_tax_reduction_working_age_unearned_income_tax) + add to the family's income tax, so a Step 6 tax reduction neither disappears + nor counts twice. Each example puts every family and its variants in one simulation, so the formulas also run on arrays with several benefit units and households. @@ -177,6 +182,54 @@ def test_income_related_esa_passports_to_the_full_liability(units): assert np.allclose(award, liability, atol=0.01) +@SETTINGS +@given( + st.lists( + st.tuples( + st.sampled_from(REGIONS), + st.booleans(), + st.integers(0, 60_000), + st.integers(0, 40_000), + ), + min_size=1, + max_size=8, + ) +) +def test_tax_on_earnings_and_on_a_pension_add_to_the_income_tax(rows): + # Invariant 7. A partner with no income gives a basic-rate earner Marriage + # Allowance, a Step 6 tax reduction. + units = [] + for region, couple, pay, pension in rows: + people = [ + { + "age": 40, + "employment_income": pay, + "private_pension_income": pension, + "council_tax_benefit_reported": 100, + } + ] + if couple: + people.append({"age": 40}) + units.append( + { + "region": region, + "people": people, + "would_claim_uc": False, + "rent": 0, + "council_tax": 1_800, + "savings": 0, + "esa_income": 0, + } + ) + sim = Simulation(situation=situation(units)) + income_tax = sim.calculate("income_tax", YEAR, map_to="benunit") + on_earnings = sim.calculate("uc_income_tax_on_earnings", YEAR, map_to="benunit") + on_unearned = sim.calculate( + "council_tax_reduction_working_age_unearned_income_tax", YEAR + ) + np.testing.assert_allclose(on_earnings + on_unearned, income_tax, atol=0.01) + + def welsh_universal_credit_family(earnings, rent, statutory_maternity_pay=0): return { "region": "WALES", diff --git a/policyengine_uk/tests/test_council_tax_reduction_pensioner_children_tariff_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pensioner_children_tariff_properties.py index 15c9896e4c..fcfd1a8dff 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pensioner_children_tariff_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pensioner_children_tariff_properties.py @@ -362,7 +362,12 @@ def test_northern_ireland_gets_no_new_amounts(): (14_071.2, 17_170, 1_380.24), (9_412, 9_592.34, 1_963.93), (3_801.2, 16_080.24, 889.22), - (5_972.2, 24_674.64, 460.78), + # The Scottish couple on 25,000 of pay. The Marriage Allowance comes + # off the tax at Step 6, so taxable income stays 12,500 and its last + # 350 reach the 21% band from 12,150: 380 + 2,030 + 73.50 - 250 = + # 2,233.50 of tax. With the allowance off taxable income the tax was + # 2,230, so income falls by 3.50 and the award rises by 20% of that. + (5_972.2, 24_671.14, 461.48), (3_801.2, 9_371.84, 937.87), (9_412, 12_000, 1_482.4), (14_071.2, 17_150, 1_384.24), diff --git a/policyengine_uk/tests/test_gift_aid.py b/policyengine_uk/tests/test_gift_aid.py index 34d7bb182c..7beb9f00b4 100644 --- a/policyengine_uk/tests/test_gift_aid.py +++ b/policyengine_uk/tests/test_gift_aid.py @@ -371,7 +371,12 @@ def test_zero_gift_preserves_baseline_outputs_bit_for_bit(): "HIGHER", "ADDITIONAL", "HIGHER", - "INTERMEDIATE", + # The model allocates the allowance to all 10,000 of property + # income, so no income bears a Scottish rate (ITA 2007 s. 11A). The + # remaining 27,430 of savings and dividends falls below the 37,700 + # UK basic rate limit (ss. 11D(6) and 13(5)), so tax_band returns + # BASIC (#2350). + "BASIC", "ADDITIONAL", ] diff --git a/policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py b/policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py index 6cc429a92b..343d504245 100644 --- a/policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py +++ b/policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py @@ -449,10 +449,13 @@ def test_the_step_lists_are_disjoint_and_hold_the_modelled_provisions(): @pytest.mark.parametrize("year", [2026, 2027, 2028, 2029, 2030]) -def test_step_6_puts_the_pension_tax_allowance_after_every_other_reduction(sim, year): +def test_step_6_lists_the_pension_and_marriage_allowances_last(sim, year): income_tax = sim.tax_benefit_system.parameters(year).gov.hmrc.income_tax reductions = list(income_tax.step_6_tax_reductions) - assert reductions[-1] == "pension_tax_allowance" + assert reductions[-2:] == [ + "pension_tax_allowance", + "marriage_allowance_tax_reduction", + ] for index, reduction in enumerate(reductions): assert earlier_tax_reductions(income_tax, reduction) == reductions[:index] diff --git a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py index caeacd7ce2..6c305411ee 100644 --- a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py +++ b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py @@ -24,12 +24,13 @@ one has a Marriage Allowance, and income tax is what it is at current law when no one claims the allowance. 3. With any statutory rate and the CPS parameters at their defaults, no one - in the higher or additional band meets the income conditions or has the - allowance, and no one has more than the statutory share of the personal - allowance, rounded up to the next £10. + liable above the basic rates (above the Scottish intermediate rate for a + Scottish taxpayer) meets the income conditions or has the allowance, + everyone else meets the conditions, and no one has more than the statutory + share of the personal allowance, rounded up to the next £10. 4. The allowance never falls when the statutory rate rises. -5. Switched on by its own parameters, the reform behaves as it did before - this change (regression values computed before the fix). +5. Switched on by its own parameters, the reform gives the allowance to the + higher-rate earner too, worth the basic rate of the allowance. """ import functools @@ -69,6 +70,12 @@ # (marriage_allowance/rounding_increment.yaml). PERSONAL_ALLOWANCE = 12_570 ROUNDING_INCREMENT = 10 +# 2025-26 taxable income above which the model charges non-savings income at +# rates that bar the allowance: the basic rate limit (ITA 2007 s. 10), and for +# a Scottish taxpayer the start of the Scottish higher rate, £43,662 of income +# less the personal allowance (Scottish Rate Resolution 2025). +BASIC_RATE_LIMIT = 37_700 +SCOTTISH_HIGHER_RATE_START = 31_092 REFORM_VARIABLE = "meets_expanded_ma_conditions" # Only the reform adds it. @@ -161,19 +168,19 @@ def test_a_higher_statutory_rate_raises_only_the_statutory_allowance(route): assert outcome(simulation) == ([2_520, 0, 0, 0], [2_982, 11_432]) -# Invariant 5. Each expectation was computed on main before this change -# (ecd531776); the reform's own parameters switched it on there too. The -# expanded allowance reaches the higher-rate earner. The model takes the -# allowance off taxable income, so it is worth 40% to them: 11,432 - 1,260 x -# 40% = 10,928 at 10%, and 11,432 - 2,520 x 40% = 10,424 at 20%. +# Invariant 5, by hand. The expanded allowance reaches the higher-rate earner. +# Parliament gives the allowance as a tax reduction of the basic rate on the +# amount (ITA 2007 s. 55B(1) and (3)), whatever the recipient's top rate: +# 11,432 - 1,260 x 20% = 11,180 at 10%, and 11,432 - 2,520 x 20% = 10,928 at +# 20%. OWN_SWITCHES = { "remove_income_condition": ( change((REMOVE_INCOME_CONDITION, True)), - ([1_260, 0, 1_260, 0], [3_234, 10_928]), + ([1_260, 0, 1_260, 0], [3_234, 11_180]), ), "cps_rate": ( change((CPS_RATE, 0.2)), - ([2_520, 0, 2_520, 0], [2_982, 10_424]), + ([2_520, 0, 2_520, 0], [2_982, 10_928]), ), } @@ -192,7 +199,7 @@ def test_a_cps_rate_switches_the_reform_on_whatever_the_statutory_rate(): reform = change((STATUTORY_RATE, 0.2), (CPS_RATE, 0.2)) simulation = Simulation(situation=ISSUE_COUPLES, reform=reform) assert REFORM_VARIABLE in simulation.tax_benefit_system.variables - assert outcome(simulation) == ([2_520, 0, 2_520, 0], [2_982, 10_424]) + assert outcome(simulation) == ([2_520, 0, 2_520, 0], [2_982, 10_928]) CHILD_CONDITION_COUPLES = couples( @@ -281,8 +288,8 @@ def test_the_reform_switches_on_from_its_own_parameters_only( # parameters, reused: each example writes its own inputs. COUPLES = 20 -# Every fourth couple lives in Scotland, whose starter and intermediate bands -# are eligible bands. +# Every fourth couple lives in Scotland, where Parliament allows the allowance +# at the Scottish starter and intermediate rates (ITA 2007 s. 55B(2)(b)). REGIONS = ["SCOTLAND", "LONDON", "NORTH_EAST", "WALES"] @@ -368,7 +375,6 @@ def run(simulation, drawn, anyone_claims=True): simulation.set_input("is_married", YEAR, married) simulation.set_input("would_claim_marriage_allowance", YEAR, claims) simulation.reset_calculations() - p = simulation.tax_benefit_system.parameters(YEAR) return dict( allowance=simulation.calculate("marriage_allowance", YEAR), meets_income_conditions=simulation.calculate( @@ -376,8 +382,15 @@ def run(simulation, drawn, anyone_claims=True): ), # Simulation.calculate returns an enum's names. band=np.asarray(simulation.calculate("tax_band", YEAR), dtype=str), - eligible_bands=p.gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands, income_tax=simulation.calculate("income_tax", YEAR), + # The earners have only employment income, so they have no savings or + # dividend income. + taxable=np.maximum( + simulation.calculate("employment_income", YEAR) + - simulation.calculate("personal_allowance", YEAR), + 0, + ), + scottish=simulation.calculate("pays_scottish_income_tax", YEAR), ) @@ -408,11 +421,14 @@ def test_any_statutory_rate_keeps_the_statutory_conditions_and_cap( simulation = simulation_for((STATUTORY_RATE, rate_permille / 1_000)) assert REFORM_VARIABLE not in simulation.tax_benefit_system.variables r = run(simulation, drawn) - outside = ~np.isin(r["band"], r["eligible_bands"]) - higher = np.isin(r["band"], ["HIGHER", "ADDITIONAL"]) - assert outside[higher].all() - assert not r["meets_income_conditions"][outside].any() - assert (r["allowance"][outside] == 0).all() + # Liable above those rates on any of their income, however little (ITA + # 2007 s. 55B(2)(b)). Income at the limit bears the lower rate (s. 10(2)). + limit = np.where(r["scottish"], SCOTTISH_HIGHER_RATE_START, BASIC_RATE_LIMIT) + above = r["taxable"] > limit + assert np.isin(r["band"], ["HIGHER", "ADDITIONAL"])[above].all() + assert not r["meets_income_conditions"][above].any() + assert r["meets_income_conditions"][~above].all() + assert (r["allowance"][above] == 0).all() assert (r["allowance"] >= 0).all() assert (r["allowance"] <= statutory_cap(rate_permille)).all() diff --git a/policyengine_uk/tests/test_marriage_allowance_tax_reduction_properties.py b/policyengine_uk/tests/test_marriage_allowance_tax_reduction_properties.py new file mode 100644 index 0000000000..045ef63b27 --- /dev/null +++ b/policyengine_uk/tests/test_marriage_allowance_tax_reduction_properties.py @@ -0,0 +1,194 @@ +"""The Marriage Allowance gives its recipient a tax reduction at Step 6. + +Parliament gives the recipient a tax reduction of the appropriate percentage +of the transferable amount (ITA 2007 s. 55B(1)) at Step 6 (s. 55A(2)): the +basic rate, or for a Scottish taxpayer the Scottish basic rate (s. 55B(3)). +The reduction comes off only so far as Step 5 tax remains after the +reductions already deducted (s. 29(2) and (3)). + +Invariants, for generated married couples in England, Scotland, Wales and +Northern Ireland, with pay, pensions, savings and dividends drawn freely: + +1. For every person, income_tax equals the income tax with + would_claim_marriage_allowance false, less the smaller of the appropriate + percentage of marriage_allowance and the Step 5 tax the earlier reductions + leave. +2. The reduction does not depend on the recipient's marginal rate: under the + CPS expanded reform, which gives the allowance to recipients in every band, + a recipient with enough tax gets exactly the appropriate percentage of the + allowance, and income tax falls by that amount. +3. The election does not move the rate test: + meets_marriage_allowance_income_conditions gives the same result with + would_claim_marriage_allowance true and false. +4. The reduction lies between zero and the appropriate percentage of the + allowance, and the Step 6 reductions together never exceed the Step 5 tax. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.reforms.cps.marriage_tax_reforms import expanded_ma_reform + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +TOLERANCE = 0.01 +COUPLES = 8 +REGIONS = ["LONDON", "SCOTLAND", "WALES", "NORTHERN_IRELAND"] +YEARS = [2016, 2017, 2018, 2021, 2025, 2026] + +small = st.integers(min_value=0, max_value=20_000) +person_strategy = st.tuples( + st.integers(min_value=0, max_value=160_000), # employment income + st.integers(min_value=0, max_value=30_000), # private pension income + small, # savings interest + small, # dividends +) +couple_strategy = st.tuples(person_strategy, person_strategy, st.sampled_from(REGIONS)) +couples_strategy = st.lists(couple_strategy, min_size=COUPLES, max_size=COUPLES) + +VARIABLES = [ + "income_tax", + "income_tax_pre_charges", + "income_tax_reductions", + "marriage_allowance", + "marriage_allowance_tax_reduction", + "marriage_allowance_appropriate_percentage", + "meets_marriage_allowance_income_conditions", +] + + +def situation(couples, year, would_claim): + people, benunits, households = {}, {}, {} + for i, (first, second, region) in enumerate(couples): + members = [f"first_{i}", f"second_{i}"] + for name, (pay, pension, savings, dividends) in zip(members, (first, second)): + people[name] = { + "age": {year: 45}, + "marital_status": {year: "MARRIED"}, + "employment_income": {year: pay}, + "private_pension_income": {year: pension}, + "savings_interest_income": {year: savings}, + "dividend_income": {year: dividends}, + "would_claim_marriage_allowance": {year: would_claim}, + } + benunits[f"benunit_{i}"] = {"members": members, "is_married": {year: True}} + households[f"household_{i}"] = { + "members": members, + "region": {year: region}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(couples, year, would_claim, reform=None): + simulation = Simulation( + situation=situation(couples, year, would_claim), reform=reform + ) + values = {name: np.asarray(simulation.calculate(name, year)) for name in VARIABLES} + step_6 = simulation.tax_benefit_system.parameters( + year + ).gov.hmrc.income_tax.step_6_tax_reductions + earlier = list(step_6)[: list(step_6).index("marriage_allowance_tax_reduction")] + values["earlier_reductions"] = sum( + np.asarray(simulation.calculate(name, year)) for name in earlier + ) + values["step_6_reductions"] = sum( + np.asarray(simulation.calculate(name, year)) for name in step_6 + ) + return values + + +SETTINGS = settings( + max_examples=12, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@SETTINGS +@given(couples=couples_strategy, year=st.sampled_from(YEARS)) +def test_reduction_takes_the_appropriate_percentage_off_step_5_tax(couples, year): + claimed = calculate(couples, year, would_claim=True) + unclaimed = calculate(couples, year, would_claim=False) + rate = claimed["marriage_allowance_appropriate_percentage"] + allowance = claimed["marriage_allowance"] + reduction = claimed["marriage_allowance_tax_reduction"] + tax_left = np.maximum( + 0, unclaimed["income_tax_pre_charges"] - unclaimed["earlier_reductions"] + ) + expected = np.minimum(rate * allowance, tax_left) + # Invariant 1. + np.testing.assert_allclose( + claimed["income_tax"], unclaimed["income_tax"] - expected, atol=TOLERANCE + ) + np.testing.assert_allclose(reduction, expected, atol=TOLERANCE) + # Invariant 3. + np.testing.assert_array_equal( + claimed["meets_marriage_allowance_income_conditions"], + unclaimed["meets_marriage_allowance_income_conditions"], + ) + # Invariant 4. + assert (reduction >= -TOLERANCE).all() + assert (reduction <= rate * allowance + TOLERANCE).all() + step_5 = np.maximum(0, claimed["income_tax_pre_charges"]) + assert (claimed["step_6_reductions"] <= step_5 + TOLERANCE).all() + assert (claimed["income_tax_reductions"] <= step_5 + TOLERANCE).all() + # The allowance never changes the Step 5 tax. + np.testing.assert_allclose( + claimed["income_tax_pre_charges"], + unclaimed["income_tax_pre_charges"], + atol=TOLERANCE, + ) + + +@SETTINGS +@given(couples=couples_strategy, year=st.sampled_from([2018, 2021, 2025, 2026])) +def test_reduction_does_not_depend_on_the_marginal_rate(couples, year): + # Invariant 2. The CPS expanded reform, with no child condition, gives the + # allowance to married recipients in every band. + claimed = calculate(couples, year, would_claim=True, reform=expanded_ma_reform) + unclaimed = calculate(couples, year, would_claim=False, reform=expanded_ma_reform) + rate = claimed["marriage_allowance_appropriate_percentage"] + allowance = claimed["marriage_allowance"] + full = rate * allowance + enough_tax = ( + unclaimed["income_tax_pre_charges"] - unclaimed["earlier_reductions"] + >= full + TOLERANCE + ) + np.testing.assert_allclose( + claimed["marriage_allowance_tax_reduction"][enough_tax], + full[enough_tax], + atol=TOLERANCE, + ) + np.testing.assert_allclose( + unclaimed["income_tax"][enough_tax] - claimed["income_tax"][enough_tax], + full[enough_tax], + atol=TOLERANCE, + ) + # The appropriate percentage is the basic rate or the Scottish basic rate, + # 20% in each of these years, whatever the recipient's income. + np.testing.assert_allclose(rate, 0.2) + + +def test_higher_rate_recipients_under_the_expanded_reform_get_20_percent(): + # 2025-26, England: pay of 60,000 and 150,000 put the recipients in the + # higher and additional rate bands. The CPS expanded reform gives each the + # 1,260 allowance, and the reduction is 20% x 1,260 = 252, not 40% or 45% + # of it. + couples = [ + ((60_000, 0, 0, 0), (0, 0, 0, 0), "LONDON"), + ((150_000, 0, 0, 0), (0, 0, 0, 0), "LONDON"), + ((60_000, 0, 0, 0), (0, 0, 0, 0), "SCOTLAND"), + ] + claimed = calculate(couples, 2025, would_claim=True, reform=expanded_ma_reform) + unclaimed = calculate(couples, 2025, would_claim=False, reform=expanded_ma_reform) + np.testing.assert_allclose( + claimed["marriage_allowance"], [1_260, 0, 1_260, 0, 1_260, 0] + ) + np.testing.assert_allclose( + unclaimed["income_tax"] - claimed["income_tax"], + [252, 0, 252, 0, 252, 0], + atol=TOLERANCE, + ) diff --git a/policyengine_uk/tests/test_property_income_band_base_properties.py b/policyengine_uk/tests/test_property_income_band_base_properties.py index 7d5730f867..98abf4cfd7 100644 --- a/policyengine_uk/tests/test_property_income_band_base_properties.py +++ b/policyengine_uk/tests/test_property_income_band_base_properties.py @@ -10,9 +10,10 @@ amount leaves savings and dividend tax unchanged, in England and in Scotland. (The taxable amount, because the £1,000 property allowance can relieve small property income in full.) -2. Accounting: with no Marriage Allowance, non_savings_taxable_income equals - adjusted net income less savings, dividends and allowances, floored at nil - (earned_taxable_income also subtracts Marriage Allowance; #1947). +2. Accounting: non_savings_taxable_income equals adjusted net income less + savings, dividends and allowances, floored at nil. The Marriage Allowance + does not enter it: the model gives the allowance as a tax reduction at + Step 6 (ITA 2007 s. 55B(1)). 3. Monotonic: more property income never lowers savings or dividend tax. (Main passes this too; the differential and the higher-rate check below are what catch the bug.) diff --git a/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py b/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py new file mode 100644 index 0000000000..661341fbcf --- /dev/null +++ b/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py @@ -0,0 +1,56 @@ +"""Saved policies naming the Marriage Allowance eligible bands get directions. + +The model applies the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to +income directly, so nothing reads +``gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands`` (#2350). +""" + +import pytest +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation +from policyengine_uk.system import system +from policyengine_uk.utils.parameters import RemovedParameterNode +from policyengine_uk.utils.scenario import Scenario + +REMOVED = "gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands" +SITUATION = {"people": {"person": {"age": {2025: 40}}}} +# The parameter held a list of band names, as a saved policy would. +SAVED = {REMOVED: {"2025": ["NONE", "STARTER", "BASIC"]}} + + +@pytest.fixture(scope="module", autouse=True) +def reuse_country_model(): + with pytest.MonkeyPatch.context() as patch: + patch.setattr( + "policyengine_uk.simulation.CountryTaxBenefitSystem", system.clone + ) + yield + + +@pytest.fixture(scope="module") +def baseline(): + return Simulation(situation=SITUATION) + + +@pytest.mark.parametrize("entry_point", ["core_reform", "scenario_reform"]) +def test_removed_eligible_bands_give_directions(baseline, entry_point): + simulation = baseline.clone() + with pytest.raises(ValueError) as error: + if entry_point == "core_reform": + Reform.from_dict(SAVED)(simulation.tax_benefit_system) + else: + Scenario.from_reform(SAVED).apply(simulation) + message = str(error.value) + assert f"{REMOVED} has been removed" in message + assert "s. 55B(2)(b)" in message + assert "gov.hmrc.income_tax.rates.uk[1].threshold" in message + assert "gov.hmrc.income_tax.rates.scotland.rates[3].threshold" in message + + +def test_removed_eligible_bands_stay_discoverable(baseline): + marriage_allowance = baseline.tax_benefit_system.parameters.gov.hmrc.income_tax.allowances.marriage_allowance + alias = marriage_allowance.children["eligible_bands"] + assert isinstance(alias, RemovedParameterNode) + assert alias.metadata["removed"] is True + assert "s. 55B(2)(b)" in alias.description diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py new file mode 100644 index 0000000000..e8e7dcae64 --- /dev/null +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -0,0 +1,445 @@ +"""Property tests for a Scottish taxpayer's tax_band and the Marriage +Allowance income conditions (#2350). + +tax_band returns the highest band in which a person has taxable income. For a +Scottish taxpayer the liability charges income other than savings and +dividends at the Scottish rates (earned_income_tax and property_income_tax, +ITA 2007 s. 11A) and savings and dividends at the rest-of-UK savings and +dividend rates (savings_income_tax and dividend_income_tax, ss. 11D(6) and +13(5)). These tests read the band off that liability: they raise the rates of +one band at a time by RISE and see which bands' rates each person pays. + +These tests use single adults, so the model grants them no Marriage +Allowance. We set the starting-rate amount, Personal Savings Allowance and +dividend allowance to zero, so the model taxes every taxable pound at a band's +positive rate, and for every single Scottish taxpayer: + +- a person in a band pays tax at that band's rates on some income and at no + higher band's rates, and a person with no band pays no tax; +- the Marriage Allowance income conditions hold exactly when the person pays + nothing at the higher or additional rates (s. 55B(2)(b)). + +With the baseline nil-rate amounts, the model keeps nil-rate income in its +underlying band, and no one pays tax at the higher or additional rates above +their band. + +The Marriage Allowance income conditions do not read the band. With the +baseline nil-rate amounts and with reformed limits and amounts, for every +single person in England, and in Scotland from 2017-18, they hold exactly when +the liability charges no income at a rate s. 55B(2)(b) forbids and, with the +dividend allowance removed (s. 55B(2)(ba)), none at the dividend upper or +additional rate. So savings at the savings nil rate or the starting rate for +savings never stop the allowance. The reforms start the dividend upper rate at +the basic rate limit, where Parliament starts it (s. 13(2)), and hold the +starting rate limit for savings at or below the basic rate limit. In that +range the model's savings liability places the savings nil rate where +Parliament does (s. 12A). + +Before #2350 the band compared all taxable income with the Scottish limits. +Against that formula, which ran from 2019-20, the band never rises, except +from STARTER to BASIC where savings or dividends sit below the Scottish +starter limit: they sit in the rest-of-UK basic band (where the model charges +the starting rate for savings, the dividend nil rate or the dividend ordinary +rate), not at the Scottish starter rate. +""" + +from contextlib import contextmanager + +import numpy as np +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +# Whole-pound limits and allowances, with Scottish schedules of three (2017-18), +# five (2018-19 to 2023-24) and six rates (from 2024-25). +YEARS = list(range(2017, 2027)) +PEOPLE = 16 +# Raising a band's rates by 100 percentage points measures the income the +# model taxes at those rates. These inputs use whole pounds, and the model +# rounds the tapered personal allowance up to a pound. SOME allows for float32 +# rounding. +RISE = 1.0 +SOME = 0.25 +BANDS = ["NONE", "STARTER", "BASIC", "INTERMEDIATE", "HIGHER", "ADDITIONAL"] +RANK = {band: rank for rank, band in enumerate(BANDS)} +SCOTTISH = "gov.hmrc.income_tax.rates.scotland.rates[{}].rate" +UK = "gov.hmrc.income_tax.rates.uk[{}].rate" +SAVINGS = "gov.hmrc.income_tax.rates.savings.{}" +DIVIDENDS = "gov.hmrc.income_tax.rates.dividends[{}].rate" +# The rates charged in each band: the Scottish rates on non-savings income, +# the savings rates and the dividend rates. ADDITIONAL covers the Scottish +# advanced and top rates. +BAND_RATES = { + "STARTER": [SCOTTISH.format(0)], + "BASIC": [SCOTTISH.format(1), SAVINGS.format("basic"), DIVIDENDS.format(0)], + "INTERMEDIATE": [SCOTTISH.format(2)], + "HIGHER": [SCOTTISH.format(3), SAVINGS.format("higher"), DIVIDENDS.format(1)], + "ADDITIONAL": [ + SCOTTISH.format(4), + SCOTTISH.format(5), + SAVINGS.format("additional"), + DIVIDENDS.format(2), + DIVIDENDS.format(3), + ], +} +# The starting rate for savings, the Personal Savings Allowance and the +# dividend allowance. +NIL_RATE_AMOUNTS = [ + "gov.hmrc.income_tax.rates.savings_starter_rate.allowance", + "gov.hmrc.income_tax.allowances.personal_savings_allowance.basic", + "gov.hmrc.income_tax.allowances.personal_savings_allowance.higher", + "gov.hmrc.income_tax.allowances.personal_savings_allowance.additional", + "gov.hmrc.income_tax.allowances.dividend_allowance", +] +LIABILITY = [ + "earned_income_tax", + "property_income_tax", + "savings_income_tax", + "dividend_income_tax", +] +INCOMES = [ + "employment_income", + "property_income", + "savings_interest_income", + "dividend_income", + "gift_aid", +] + +pounds = st.integers(min_value=0, max_value=60_000) +# Pay at the 2025-26 Scottish and rest-of-UK limits, the taper and the top. +pay = st.one_of( + st.integers(min_value=0, max_value=160_000), + st.sampled_from([12_570, 15_397, 27_491, 43_662, 50_270, 75_000, 100_001]), +) +person = st.tuples( + pay, + st.one_of(st.just(0), pounds), # property income + st.one_of(st.just(0), pounds), # savings interest + st.one_of(st.just(0), pounds), # dividends + # Gift Aid in multiples of £4 grosses up to whole pounds at 20%. + st.one_of( + st.just(0), st.integers(min_value=0, max_value=1_000).map(lambda n: 4 * n) + ), +) +people = st.lists(person, min_size=PEOPLE, max_size=PEOPLE) +year = st.sampled_from(YEARS) + +# The issue's cases, 2025-26: pay and dividends of 40,000 and 5,000, 60,000 and +# 20,000, and 30,000 and 25,000; the same with savings; both limits at once. +ISSUE = [ + (40_000, 0, 0, 5_000, 0), + (60_000, 0, 0, 20_000, 0), + (30_000, 0, 0, 25_000, 0), + (40_000, 0, 5_000, 0, 0), + (60_000, 0, 20_000, 0, 0), + (30_000, 0, 25_000, 0, 0), + (43_662, 0, 0, 6_608, 0), + (5_000, 0, 1_000, 1_000, 0), +] +ISSUE += [(0, 0, 0, 0, 0)] * (PEOPLE - len(ISSUE)) + + +def situation(): + return { + "people": {f"person_{i}": {"age": {YEARS[0]: 40}} for i in range(PEOPLE)}, + "benunits": { + f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + "households": { + f"household_{i}": { + "members": [f"person_{i}"], + "country": {year: "SCOTLAND" for year in YEARS}, + } + for i in range(PEOPLE) + }, + } + + +@pytest.fixture(scope="module") +def sim(): + return Simulation(situation=situation()) + + +def set_people(sim, drawn, period): + for variable, values in zip(INCOMES, zip(*drawn)): + sim.set_input(variable, period, np.array(values, dtype=float)) + sim.reset_calculations() + + +def set_law(sim, period, values): + """Set parameter values for the year on the simulation's own system.""" + parameters = sim.tax_benefit_system.parameters + for path, value in values.items(): + parameters.get_child(path).update(period=str(period), value=value) + sim.tax_benefit_system.reset_parameter_caches() + sim.reset_calculations() + + +@contextmanager +def law(sim, period, values): + parameters = sim.tax_benefit_system.parameters + before = {path: parameters.get_child(path)(str(period)) for path in values} + set_law(sim, period, values) + try: + yield + finally: + set_law(sim, period, before) + + +def liability(sim, period): + return sum(sim.calculate(v, period).astype(float) for v in LIABILITY) + + +def charged_at(sim, period, paths): + """Measure the income the model taxes at these rates by raising only + these rates by RISE and dividing the tax increase by RISE.""" + parameters = sim.tax_benefit_system.parameters + before = liability(sim, period) + rates = {path: parameters.get_child(path)(str(period)) for path in paths} + # Skip Scottish rates that Parliament had not set that year. + raised = {path: rate + RISE for path, rate in rates.items() if rate is not None} + with law(sim, period, raised): + return (liability(sim, period) - before) / RISE + + +def charged(sim, period, bands): + """The income charged at each band's rates.""" + return {band: charged_at(sim, period, BAND_RATES[band]) for band in bands} + + +def band_of(sim, period): + return np.asarray(sim.calculate("tax_band", period), dtype=str) + + +SETTINGS = settings( + max_examples=8, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +@SETTINGS +@given(people, year) +@example(ISSUE, 2025) +def test_band_is_the_highest_rate_charged_without_nil_rates(sim, drawn, period): + set_people(sim, drawn, period) + with law(sim, period, {path: 0 for path in NIL_RATE_AMOUNTS}): + band = band_of(sim, period) + meets = np.asarray( + sim.calculate("meets_marriage_allowance_income_conditions", period) + ) + income_at = charged(sim, period, BAND_RATES) + for i, b in enumerate(band): + case = f"{drawn[i]} in {period}: {b}, {[income_at[x][i] for x in income_at]}" + if b == "NONE": + assert all(abs(income_at[x][i]) < SOME for x in income_at), case + continue + assert income_at[b][i] > SOME, case + for above in BANDS[RANK[b] + 1 :]: + assert abs(income_at[above][i]) < SOME, case + above_intermediate = (income_at["HIGHER"] > SOME) | (income_at["ADDITIONAL"] > SOME) + np.testing.assert_array_equal(meets, ~above_intermediate, err_msg=str(drawn)) + + +@SETTINGS +@given(people, year) +@example(ISSUE, 2025) +def test_no_one_pays_higher_rates_above_their_band(sim, drawn, period): + set_people(sim, drawn, period) + band = band_of(sim, period) + tax = liability(sim, period) + # Counts by marginal rate use these bands. + income_at = charged(sim, period, ["HIGHER", "ADDITIONAL"]) + for i, b in enumerate(band): + case = f"{drawn[i]} in {period}: {b}, {[income_at[x][i] for x in income_at]}" + if b == "NONE": + assert abs(tax[i]) < SOME, case + for above in income_at: + if RANK[above] > RANK[b]: + assert abs(income_at[above][i]) < SOME, case + + +def band_before_2350(sim, period): + """tax_band's Scottish formula from 2019-20 before #2350: all taxable + income against the Scottish limits, the next band from each limit on.""" + scale = sim.tax_benefit_system.parameters( + str(period) + ).gov.hmrc.income_tax.rates.scotland.rates + income = sim.calculate("net_income", period) - sim.calculate("allowances", period) + gift = sim.calculate("gift_aid_grossed_up", period) + return np.select( + [ + income < threshold + (gift if index >= 2 else 0) + for index, threshold in enumerate(scale.thresholds[:5]) + ], + BANDS[:5], + default="ADDITIONAL", + ) + + +@SETTINGS +@given(people, st.sampled_from(YEARS[2:])) +@example(ISSUE, 2025) +def test_band_never_rises_except_from_starter_to_basic(sim, drawn, period): + set_people(sim, drawn, period) + band = band_of(sim, period) + before = band_before_2350(sim, period) + taxable = sim.calculate("net_income", period) - sim.calculate("allowances", period) + non_savings = sim.calculate("non_savings_income_after_allowances", period) + starter_limit = sim.tax_benefit_system.parameters( + str(period) + ).gov.hmrc.income_tax.rates.scotland.rates.thresholds[1] + for i in range(PEOPLE): + if RANK[band[i]] <= RANK[before[i]]: + continue + # Savings or dividends below the starter limit sit in the basic band. + case = f"{drawn[i]} in {period}: {before[i]} to {band[i]}" + assert (before[i], band[i]) == ("STARTER", "BASIC"), case + assert non_savings[i] < taxable[i] < starter_limit, case + + +def test_savings_allowance_does_not_read_the_band(): + # Parliament sets the Personal Savings Allowance on the rest-of-UK bands + # for everyone (ITA 2007 s. 12B(8)), so savings_allowance (#2347) never + # reads tax_band: forcing the band changes nothing. + sim = Simulation(situation=situation()) + set_people(sim, ISSUE, 2025) + allowance = sim.calculate("savings_allowance", 2025) + np.testing.assert_array_equal(allowance[:6], [1_000, 500, 500, 1_000, 500, 500]) + for band in BANDS: + sim.set_input("tax_band", 2025, np.array([band] * PEOPLE)) + sim.reset_calculations() + np.testing.assert_array_equal( + sim.calculate("savings_allowance", 2025), allowance + ) + + +# The Marriage Allowance income conditions against the liability, with the +# baseline nil-rate amounts and with reformed limits and amounts: people +# alternate between Scotland and England, from 2016-17, the first year of the +# savings and dividend nil rates. Before 2017-18 the model charges a Scottish +# taxpayer's non-savings income on its 2017-18 Scottish scale, where Parliament +# charged it on the rest-of-UK limits (s. 11A(1)). The Marriage Allowance test +# uses the basic rate limit there, as Parliament did, so this test leaves those +# people out. +MA_YEARS = list(range(2016, 2027)) +MA_COUNTRIES = ["SCOTLAND", "ENGLAND"] +FIRST_SCOTTISH_LIMITS_YEAR = 2017 +# The rates Parliament forbids in s. 55B(2)(b), other than the dividend rates: +# the Scottish higher, advanced and top rates, the higher and additional rates, +# and the savings higher and additional rates. Each person pays at most one of +# the Scottish and rest-of-UK schedules on non-savings income. +FORBIDDEN = ( + [SCOTTISH.format(i) for i in (3, 4, 5)] + + [UK.format(i) for i in (1, 2, 3)] + + [SAVINGS.format("higher"), SAVINGS.format("additional")] +) +DIVIDEND_UPPER = [DIVIDENDS.format(i) for i in (1, 2, 3)] +DIVIDEND_ALLOWANCE = "gov.hmrc.income_tax.allowances.dividend_allowance" +BASIC_RATE_LIMIT = "gov.hmrc.income_tax.rates.uk[1].threshold" +DIVIDEND_UPPER_START = "gov.hmrc.income_tax.rates.dividends[1].threshold" +STARTING_RATE_LIMIT = "gov.hmrc.income_tax.rates.savings_starter_rate.allowance" +# Rate limits and nil-rate amounts a reform might change, each drawn or left +# at the law in force. +LIMITS = { + "gov.hmrc.income_tax.rates.scotland.rates[3].threshold": st.integers( + 20_000, 50_000 + ), + BASIC_RATE_LIMIT: st.integers(25_000, 50_000), + STARTING_RATE_LIMIT: st.integers(0, 60_000), + "gov.hmrc.income_tax.allowances.personal_savings_allowance.basic": st.integers( + 0, 10_000 + ), + "gov.hmrc.income_tax.allowances.personal_savings_allowance.higher": st.integers( + 0, 10_000 + ), + DIVIDEND_ALLOWANCE: st.integers(0, 10_000), +} +limits = st.fixed_dictionaries({}, optional=LIMITS) + +# The Scottish witness from the #2350 review: pay that leaves 37,700 of +# taxable income and 1 of interest, under a Scottish higher rate moved to +# 37,702. Then, in England, 500 of interest above 37,500 of taxable pay, all +# covered by the higher-rate Personal Savings Allowance, and pay at the basic +# rate limit. +NIL_RATE = [(0, 0, 0, 0, 0)] * PEOPLE +NIL_RATE[0] = (50_270, 0, 1, 0, 0) +NIL_RATE[1] = (50_070, 0, 500, 0, 0) +NIL_RATE[3] = (50_270, 0, 0, 0, 0) +WITNESS_LIMITS = {"gov.hmrc.income_tax.rates.scotland.rates[3].threshold": 37_702} + + +@pytest.fixture(scope="module") +def mixed_sim(): + situation = { + "people": {f"person_{i}": {"age": {MA_YEARS[0]: 40}} for i in range(PEOPLE)}, + "benunits": { + f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + "households": { + f"household_{i}": { + "members": [f"person_{i}"], + "country": { + year: MA_COUNTRIES[i % len(MA_COUNTRIES)] for year in MA_YEARS + }, + } + for i in range(PEOPLE) + }, + } + return Simulation(situation=situation) + + +@SETTINGS +@given(people, st.sampled_from(MA_YEARS), limits) +@example(NIL_RATE, 2025, WITNESS_LIMITS) +@example(NIL_RATE, 2025, {}) +def test_marriage_allowance_follows_the_liability_with_nil_rates( + mixed_sim, drawn, period, changes +): + sim = mixed_sim + set_people(sim, drawn, period) + parameters = sim.tax_benefit_system.parameters + # Skip Scottish rates that Parliament had not set that year. + changes = { + path: value + for path, value in changes.items() + if parameters.get_child(path)(str(period)) is not None + } + basic_rate_limit = changes.get( + BASIC_RATE_LIMIT, parameters.get_child(BASIC_RATE_LIMIT)(str(period)) + ) + # Parliament starts the dividend upper rate at the basic rate limit + # (ss. 10(3) and 13(2)), so a reformed limit moves the model's dividend + # scale too. + changes[DIVIDEND_UPPER_START] = basic_rate_limit + # The model's savings liability starts the savings nil rate where the + # starting rate for savings ends. That matches where Parliament places it + # (s. 12A) only while the starting rate limit stays at or below the basic + # rate limit. + if STARTING_RATE_LIMIT in changes: + changes[STARTING_RATE_LIMIT] = min( + changes[STARTING_RATE_LIMIT], basic_rate_limit + ) + with law(sim, period, changes): + meets = np.asarray( + sim.calculate("meets_marriage_allowance_income_conditions", period) + ) + forbidden = charged_at(sim, period, FORBIDDEN) + with law(sim, period, {DIVIDEND_ALLOWANCE: 0}): + dividends_above_basic = charged_at(sim, period, DIVIDEND_UPPER) + checked = np.array( + [ + period >= FIRST_SCOTTISH_LIMITS_YEAR + or MA_COUNTRIES[i % len(MA_COUNTRIES)] != "SCOTLAND" + for i in range(PEOPLE) + ] + ) + np.testing.assert_array_equal( + meets[checked], + ((forbidden < SOME) & (dividends_above_basic < SOME))[checked], + err_msg=f"{drawn} in {period} with {changes}", + ) diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index 2e408f7ee4..1b1aa8f821 100644 --- a/policyengine_uk/utils/parameters.py +++ b/policyengine_uk/utils/parameters.py @@ -254,6 +254,15 @@ class RemovedParameter(NamedTuple): "(the 16), or gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit " "for dates before 11 April 2011.", ), + "gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands": RemovedParameter( + "removed Marriage Allowance eligible tax bands", + "The model no longer reads tax_band for the Marriage Allowance income " + "conditions. It applies the rate conditions of ITA 2007 s. 55B(2)(b) " + "and (ba) to the person's income directly, so change " + "gov.hmrc.income_tax.rates.uk[1].threshold (the basic rate limit) or " + "gov.hmrc.income_tax.rates.scotland.rates[3].threshold (the start of " + "the Scottish higher rate).", + ), } diff --git a/policyengine_uk/utils/uc_income_tax.py b/policyengine_uk/utils/uc_income_tax.py index d8cab4feb3..38658d8956 100644 --- a/policyengine_uk/utils/uc_income_tax.py +++ b/policyengine_uk/utils/uc_income_tax.py @@ -12,6 +12,29 @@ ) +def earnings_tax_reductions(person, period, parameters): + """The Step 6 tax reductions that come off the tax on earnings first. + + All of them except the finance-cost reduction, which comes off the tax on + property income first, so only the part larger than that tax counts here. + """ + income_tax = parameters(period).gov.hmrc.income_tax + reductions = add( + person, + period, + [ + variable + for variable in income_tax.step_6_tax_reductions + if variable != "property_finance_cost_relief" + ], + ) + return reductions + max_( + 0, + person("property_finance_cost_relief", period) + - person("property_income_tax", period), + ) + + def uc_income_tax_on_earnings_slice(person, period, parameters, include_trade=True): """Income tax on the person's earnings taken as the lowest slice of income. @@ -86,18 +109,4 @@ def uc_income_tax_on_earnings_slice(person, period, parameters, include_trade=Tr # the tax on property income first. Only a part larger than that tax # (possible where Scottish rates on property income are below the # reduction's rate) comes off the tax on earnings. - reductions = add( - person, - period, - [ - variable - for variable in income_tax.step_6_tax_reductions - if variable != "property_finance_cost_relief" - ], - ) - reductions += max_( - 0, - person("property_finance_cost_relief", period) - - person("property_income_tax", period), - ) - return max_(0, tax - reductions) + return max_(0, tax - earnings_tax_reductions(person, period, parameters)) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py index 8ac7b852c4..44d239318a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py @@ -42,9 +42,14 @@ def formula(person, period, parameters): scottish_basic_rate, rates.uk.rates[0], ) - # Chapter 2 (personal and blind person's allowances) and Chapter 3A - # (marriage allowance, which the model deducts from income) relieve - # income; Chapter 3 (married couple's allowance) reduces the tax. + # Chapter 2 (personal and blind person's allowances) relieves income. + # Chapter 3 (married couple's allowance) reduces the tax, and so does + # Chapter 3A: the Marriage Allowance gives a reduction of the basic + # rate, or the Scottish basic rate, on the transferable amount + # (ITA 2007 s. 55B(1) and (3)). At this single notional rate, with + # both floored at zero, deducting the amount from the profit gives the + # same tax as that reduction: rate * max(0, profit - reliefs - amount) + # equals max(0, rate * max(0, profit - reliefs) - rate * amount). reliefs = add( person, period, diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py index d54f76158a..75ea9edc77 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py @@ -20,6 +20,12 @@ class marriage_allowance(Variable): value_type = float entity = Person label = "Marriage Allowance" + documentation = ( + "The amount of personal allowance transferred to this person. It does " + "not reduce their taxable income: marriage_allowance_tax_reduction " + "gives them a tax reduction of the appropriate percentage of it at " + "Step 6 (ITA 2007 s. 55B(1))." + ) definition_period = YEAR reference = "https://www.legislation.gov.uk/ukpga/2007/3/part/3/chapter/3A" unit = GBP diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py new file mode 100644 index 0000000000..d4ade32ba5 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py @@ -0,0 +1,46 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_appropriate_percentage(Variable): + value_type = float + entity = Person + label = "Marriage Allowance appropriate percentage" + documentation = ( + "The rate of the Marriage Allowance tax reduction: the basic rate, or " + "for a Scottish taxpayer the Scottish basic rate, whatever rates the " + "person's income bears (ITA 2007 s. 55B(3)). The Scottish rates of " + "s. 11A start in 2016-17, so before then the formula gives a Scottish " + "taxpayer the basic rate. Section 55B(3) gives a Welsh taxpayer the " + "Welsh basic rate, but the model has no Welsh rates, so a Welsh " + "taxpayer takes the basic rate." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007 s. 55B(3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Scottish Parliament motion S6M-16531 (2025-26 Scottish rates)", + href="https://www.parliament.scot/chamber-and-committees/votes-and-motions/S6M-16531", + ), + ] + unit = "/1" + + def formula(person, period, parameters): + rates = parameters(period).gov.hmrc.income_tax.rates + basic_rate = rates.uk.rates[0] + if period.start.year < 2016: + return basic_rate + # The Scottish basic rate band is the second band of the Scottish + # scale in every year: before 2018-19 the first band (the starter + # rate) has no values. Read that band itself rather than an index into + # the rates in force at the instant, which would move if a reform + # removed the starter rate. + scottish_bands = parameters.gov.hmrc.income_tax.rates.scotland.rates + scottish_basic_rate = scottish_bands.brackets[1].rate(period.start) + return where( + person("pays_scottish_income_tax", period), + scottish_basic_rate, + basic_rate, + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py new file mode 100644 index 0000000000..6fc83049ee --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py @@ -0,0 +1,45 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.income_tax_reductions import earlier_tax_reductions + + +class marriage_allowance_tax_reduction(Variable): + value_type = float + entity = Person + label = "Marriage Allowance tax reduction" + documentation = ( + "The Marriage Allowance recipient's tax reduction at Step 6 of the " + "Income Tax calculation: the appropriate percentage of the " + "transferable amount (ITA 2007 s. 55A(2) and s. 55B(1)). It does not " + "depend on the rates the recipient's income bears. The formula " + "deducts it after the reductions listed before it in " + "step_6_tax_reductions, and only so far as there is Step 5 tax left " + "(s. 29(2) and (3))." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007 s. 55A(2)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55A", + ), + dict( + title="Income Tax Act 2007 s. 55B(1) and (3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Income Tax Act 2007 s. 29(2) and (3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/29", + ), + ] + unit = GBP + + def formula(person, period, parameters): + income_tax = parameters(period).gov.hmrc.income_tax + reduction = person("marriage_allowance_appropriate_percentage", period) * ( + person("marriage_allowance", period) + ) + earlier = earlier_tax_reductions(income_tax, "marriage_allowance_tax_reduction") + tax_left = max_( + 0, + person("income_tax_pre_charges", period) - add(person, period, earlier), + ) + return min_(max_(0, reduction), tax_left) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py index e91f14cbd3..3089892bd2 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py @@ -1,18 +1,126 @@ from policyengine_uk.model_api import * -import numpy as np + + +def liable_only_at_marriage_allowance_rates(person, period, parameters): + """Whether the person pays no income tax above the basic rates. + + Apply the rates Parliament permits for the tax year (s. 55B(2)(b) and + s. 55C(1)(c)). From 2016-17 these include the savings nil rate; from + 2018-19 the Scottish list includes rates below basic and the intermediate + rate. Where s. 55B(2)(ba) and s. 55C(1)(ca) apply, test dividends as if + s. 13A were omitted. + + Parliament extends the rate limits by the grossed-up Gift Aid gift + (s. 414(2)(b)); the gift does not reduce taxable income. Before 2017-18 + this test uses the basic rate limit for Scottish taxpayers, even though + the model's historical Scottish liability currently reads the 2017-18 + schedule. The test also follows the statute where the model's liability + departs from it: it starts the dividend upper rate at the basic rate + limit (ss. 10 and 13), counts no savings nil rate before 2016-17 (FA 2016 + s. 4(17)), and places the nil-rate slice above the starting rate limit + for savings (s. 12A). + + Parliament charges tax at each rate at Step 4 and takes tax reductions + off at Step 6 (s. 23), and it gives the allowance as a tax reduction at + Step 6 (ss. 55A(2) and 55B(1)). So the allowance never changes the income + each rate applies to, and this test never reads it: + marriage_allowance_tax_reduction takes the allowance off the tax, and + the test reads income after allowances and the savings, dividend and + Gift Aid amounts directly. + """ + rates = parameters(period).gov.hmrc.income_tax.rates + gross_gift = person("gift_aid_grossed_up", period) + # Earnings, pensions, property and other non-savings income after + # allowances, which go to it first: the income below savings and + # dividends (s. 16). + non_savings = person("non_savings_income_after_allowances", period) + # The Scottish higher rate follows the starter, basic and intermediate + # rates; in 2017-18 the Scottish scale had only the basic rate below it. + # Before 2017-18 the test holds a Scottish taxpayer to the basic rate + # limit. The Scottish rates of s. 11A start in 2016-17 (FA 2014 Sch. 38 + # para. 12, SI 2015/2000 art. 3). That year Parliament charged the Scottish + # basic rate on income that would otherwise bear the basic rate + # (s. 11A(1)). The model reads its Scottish scale's 2017-18 values in + # earlier years too, so the test sets the limit by the year. + scottish = rates.scotland.rates + basic_rate_limit = rates.uk.thresholds[1] + gross_gift + if period.start.year < 2017 or len(scottish.rates) < 3: + scottish_limit = basic_rate_limit + elif len(scottish.rates) == 3: + scottish_limit = scottish.thresholds[1] + gross_gift + else: + scottish_limit = scottish.thresholds[3] + gross_gift + non_savings_limit = where( + person("pays_scottish_income_tax", period), + scottish_limit, + basic_rate_limit, + ) + # Savings sit above non-savings income (s. 16) and take the rest-of-UK + # limits (s. 11D(6)). Up to the basic rate limit they bear a basic rate or + # the starting rate for savings, both permitted: Parliament charges the + # starting rate only on savings that would otherwise bear a basic rate + # (s. 12(1), and s. 12(1)(b) from 2017-18). Above the limit it permits only + # the savings nil rate (s. 55B(2)(b)), which it charges on the lowest + # savings above the starting rate limit for savings, up to the savings + # allowance (s. 12A(1) to (4)); dividends sit above savings (s. 16) and + # don't move this slice. The person fails the test if any savings above the + # basic rate limit lie outside the slice. + savings = max_( + 0, + person("taxable_savings_interest_income", period) + - person("received_allowances_savings_income", period), + ) + # Parliament gave the savings nil rate effect from 2016-17 (FA 2016 + # s. 4(17)). + if period.start.year < 2016: + savings_allowance = 0 + else: + savings_allowance = person("savings_allowance", period) + below_dividends = non_savings + savings + nil_rate_start = max_(non_savings, rates.savings_starter_rate.allowance) + nil_rate_top = min_(below_dividends, nil_rate_start + savings_allowance) + savings_above_basic = max_(0, below_dividends - max_(non_savings, basic_rate_limit)) + nil_rate_above_basic = max_( + 0, nil_rate_top - max_(nil_rate_start, basic_rate_limit) + ) + savings_at_forbidden_rates = savings_above_basic - nil_rate_above_basic > 0 + # Dividends form the top slice, above all non-savings income and savings + # (s. 16); count them before the dividend allowance. Parliament charges the + # dividend upper rate on dividends that would otherwise bear the higher + # rate (s. 13(2)), and the higher rate starts at the basic rate limit plus + # the gross gift (ss. 10(3) and 414(2)(b)). + dividends = max_( + 0, + person("taxable_dividend_income", period) + - person("received_allowances_dividend_income", period), + ) + dividend_upper_limit = basic_rate_limit + dividends_above_basic = (dividends > 0) & ( + below_dividends + dividends > dividend_upper_limit + ) + return ( + (non_savings <= non_savings_limit) + & ~savings_at_forbidden_rates + & ~dividends_above_basic + ) class meets_marriage_allowance_income_conditions(Variable): label = "Meets Marriage Allowance income conditions" - documentation = "Whether this person (and their partner) meets the conditions for this person to be eligible for the Marriage Allowance, as set out in the Income Tax Act 2007 sections 55B and 55C" + documentation = ( + "Whether this person pays income tax only at rates that Parliament " + "allows for receiving the Marriage Allowance (ITA 2007 s. 55B(2)(b) " + "and (ba))." + ) entity = Person definition_period = YEAR value_type = bool - reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B" + reference = [ + dict( + title="Income Tax Act 2007 s. 55B(2)(b) and (ba)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + ] def formula(person, period, parameters): - band = person("tax_band", period) - eligible_bands = parameters( - period - ).gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands - return np.isin(band.decode_to_str(), eligible_bands) + return liable_only_at_marriage_allowance_rates(person, period, parameters) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/savings_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/savings_allowance.py index 99fe833270..e3cfc8e3cc 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/savings_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/savings_allowance.py @@ -13,11 +13,11 @@ class savings_allowance(Variable): "12B(8)(a)(iv) and (b)(iv) count as higher- or additional-rate income " "what would be charged at those rates if they were not a Scottish or " "Welsh taxpayer, so the bands are the rest-of-UK ones for everyone. " - "tax_band follows the Scottish bands for a Scottish taxpayer, as the " - "Marriage Allowance conditions need, so it is not read here. Taxable " - "income is taken, as in tax_band, as net income less allowances, and " - "a Gift Aid gift raises the basic and higher rate limits by its " - "grossed-up amount (ITA 2007 s. 414(2)(b))." + "For Scottish taxpayers, tax_band also uses Scottish limits for " + "non-savings income. This calculation uses the rest-of-UK limits " + "directly. Like tax_band's rest-of-UK band, it takes taxable income " + "as net income less allowances, and Parliament raises the basic and " + "higher rate limits by the grossed-up gift (ITA 2007 s. 414(2)(b))." ) definition_period = YEAR reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/12B" diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py index daff67c55d..67ad30d77d 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_income_after_allowances.py @@ -6,13 +6,15 @@ class non_savings_income_after_allowances(Variable): entity = Person label = "Non-savings, non-dividend income for Income Tax after allowances" documentation = ( - "Net income (before the Gift Aid step of adjusted net income, which " - "does not reduce the income charged to tax) less savings and dividend " - "income and allowances, floored at nil: the income below savings and " - "dividends (ITA 2007 s. 16) once the allowances, which go to it first, " - "are taken off. " - "property_income_tax and taxable_property_income_after_allowances " - "both read it." + "The model subtracts savings income, dividend income and allowances " + "from net income and floors the result at zero, so it sets allowances " + "against this income first. Parliament treats savings and dividends " + "as the highest part of income, above this income (ITA 2007 s. 16). " + "It raises the rate limits by the grossed-up Gift Aid gift " + "(s. 414(2)(b)), which does not reduce this taxable income. " + "property_income_tax, taxable_property_income_after_allowances, the " + "Marriage Allowance rate test and, for Scottish taxpayers, tax_band " + "read it." ) definition_period = YEAR reference = dict( diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_taxable_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_taxable_income.py index 8e1d2253a4..db6335c2c7 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_taxable_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_earned_income/non_savings_taxable_income.py @@ -12,10 +12,10 @@ class non_savings_taxable_income(Variable): "earned_taxable_income leaves property income out because " "property_income_tax taxes it separately, but property income still " "fills the bands before savings and dividends do (from 2027-28, " - "s. 16A places it immediately below savings income). With no Marriage " - "Allowance it equals non_savings_income_after_allowances; " - "earned_taxable_income also subtracts Marriage Allowance, which the " - "law gives as a tax reduction (#1947)." + "s. 16A places it immediately below savings income). It equals " + "non_savings_income_after_allowances. The Marriage Allowance does not " + "reduce it: marriage_allowance_tax_reduction gives the allowance as a " + "tax reduction at Step 6 (s. 55B(1))." ) definition_period = YEAR reference = [ diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_liability/tax_band.py b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_liability/tax_band.py index 73f78c28d5..320fd79c12 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_liability/tax_band.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_liability/tax_band.py @@ -11,68 +11,141 @@ class TaxBand(Enum): ADDITIONAL = "Additional" +def uk_tax_band_above_limits(person, period, parameters): + """Return the rest-of-UK band before 2018-19, counting income above each + limit in the next band.""" + allowances = person("allowances", period) + ani = person("net_income", period) + rates = parameters(period).gov.hmrc.income_tax.rates + gross_gift = person("gift_aid_grossed_up", period) + limits = [ + threshold + (gross_gift if index in (1, 2) else 0) + for index, threshold in enumerate(rates.uk.thresholds) + ] + basic = allowances + limits[0] + higher = allowances + limits[1] + add = allowances + limits[2] + return select( + [ani > add, ani > higher, ani > basic], + [TaxBand.ADDITIONAL, TaxBand.HIGHER, TaxBand.BASIC], + default=TaxBand.NONE, + ) + + +def scottish_tax_band(person, period, parameters): + """The highest band in which a Scottish taxpayer has taxable income. + + Use Scottish limits for income other than savings and dividends. + Parliament applies Scottish rates to this income (ITA 2007 s. 11A and the + Scottish rate resolutions). Treat savings and dividends as the top + slices of income (s. 16) and apply the rest-of-UK limits, since they + bear the rates they would bear outside Scotland (ss. 11D(6) and 13(5)). + Compare the rest-of-UK band only when taxable savings or dividends + remain, then return the higher band. Keep income at each upper limit in + the lower band. Parliament charges income "up to" a limit at the lower + rate (s. 10(2) to (3A)), and the Scottish Parliament charges each Scottish + rate "on income above" one limit "and up to" the next (each Scottish rate + resolution). Keep income charged at a nil rate in the band where it sits, + as for every taxpayer; the Marriage Allowance has its own rate test + (liable_only_at_marriage_allowance_rates). + """ + rates = parameters(period).gov.hmrc.income_tax.rates + gross_gift = person("gift_aid_grossed_up", period) + scale = rates.scotland.rates + if len(scale.thresholds) == 3: + # 2017-18: the Scottish basic, higher and additional rates. + scottish_bands = [TaxBand.BASIC, TaxBand.HIGHER, TaxBand.ADDITIONAL] + else: + # From 2018-19: the starter, basic, intermediate and higher rates, + # then the top rate, which the advanced rate precedes from 2024-25. + scottish_bands = [ + TaxBand.STARTER, + TaxBand.BASIC, + TaxBand.INTERMEDIATE, + TaxBand.HIGHER, + ] + [TaxBand.ADDITIONAL] * (len(scale.thresholds) - 4) + labels = np.array([band.index for band in [TaxBand.NONE] + scottish_bands]) + # Allowances go to this income first, as in earned_income_tax. Parliament + # gives the Marriage Allowance as a tax reduction (s. 55B(1)), so it does + # not move the band. + non_savings = person("non_savings_income_after_allowances", period) + # Parliament raises the Scottish basic rate's upper limit and every limit + # above it by the grossed-up gift (s. 414(2)(b)). + first_extended = first_extended_scottish_threshold(scale) + limits_exceeded = sum( + non_savings > threshold + (gross_gift if index >= first_extended else 0) + for index, threshold in enumerate(scale.thresholds) + ) + scottish_band = labels[limits_exceeded] + uk_limits = rates.uk.thresholds + taxable_income = person("net_income", period) - person("allowances", period) + # Savings and dividends make up the taxable income above the non-savings + # income. Parliament raises the basic and higher rate limits by the gift + # too (s. 414(2)(b)). + uk_band = select( + [ + taxable_income <= non_savings, + taxable_income > uk_limits[2] + gross_gift, + taxable_income > uk_limits[1] + gross_gift, + ], + [TaxBand.NONE.index, TaxBand.ADDITIONAL.index, TaxBand.HIGHER.index], + default=TaxBand.BASIC.index, + ) + bands = np.array(list(TaxBand), dtype=object) + return bands[max_(scottish_band, uk_band)] + + class tax_band(Variable): value_type = Enum possible_values = TaxBand default_value = TaxBand.NONE entity = Person label = "Tax band of the individual" - definition_period = YEAR - reference = dict( - title="Income Tax Act 2007, s. 10", - href="https://www.legislation.gov.uk/ukpga/2007/3/section/10", + documentation = ( + "The highest band in which the person has taxable income. For a " + "Scottish taxpayer the model takes the higher of the Scottish band " + "of their income other than savings and dividends and the rest-of-UK " + "band of their savings and dividends. Savings and dividends charged " + "at a nil rate count where they sit. ADDITIONAL covers the Scottish " + "advanced and top rates and the dividend additional rate; HIGHER " + "covers the dividend upper rate." ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007, s. 10", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/10", + ), + dict( + title="Income Tax Act 2007, s. 11A", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/11A", + ), + dict( + title="Income Tax Act 2007, s. 13(5)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/13", + ), + dict( + title="Scottish Rate Resolution 2025-26 (S6M-16531)", + href="https://www.parliament.scot/chamber-and-committees/votes-and-motions/S6M-16531", + ), + ] def formula(person, period, parameters): - allowances = person("allowances", period) - ani = person("net_income", period) - rates = parameters(period).gov.hmrc.income_tax.rates - gross_gift = person("gift_aid_grossed_up", period) - limits = [ - threshold + (gross_gift if index in (1, 2) else 0) - for index, threshold in enumerate(rates.uk.thresholds) - ] - basic = allowances + limits[0] - higher = allowances + limits[1] - add = allowances + limits[2] - return select( - [ani > add, ani > higher, ani > basic], - [TaxBand.ADDITIONAL, TaxBand.HIGHER, TaxBand.BASIC], - default=TaxBand.NONE, - ) + return uk_tax_band_above_limits(person, period, parameters) - def formula_2017_04_06(person, period, parameters): - allowances = person("allowances", period) - ani = person("net_income", period) - rates = parameters(period).gov.hmrc.income_tax.rates - scot = person("pays_scottish_income_tax", period) - income = ani - allowances - gross_gift = person("gift_aid_grossed_up", period) - # s.414(2)(b) extends the limits used to determine the tax band. - uk_band = select( - [ - income < threshold + (gross_gift if index > 0 else 0) - for index, threshold in enumerate(rates.uk.thresholds[:3]) - ], - [TaxBand.NONE, TaxBand.BASIC, TaxBand.HIGHER], - default=TaxBand.ADDITIONAL, + def formula_2017(person, period, parameters): + # Annual formulas start on 1 January, so this runs from 2017-18, when + # the Scottish Parliament first set its own band limits. + return where( + person("pays_scottish_income_tax", period), + scottish_tax_band(person, period, parameters), + uk_tax_band_above_limits(person, period, parameters), ) - first_extended = first_extended_scottish_threshold(rates.scotland.rates) - scottish_band = select( - [ - income < threshold + (gross_gift if index >= first_extended else 0) - for index, threshold in enumerate(rates.scotland.rates.thresholds[:3]) - ], - [TaxBand.NONE, TaxBand.BASIC, TaxBand.HIGHER], - default=TaxBand.ADDITIONAL, - ) - return where(scot, scottish_band, uk_band) - def formula_2018_06_01(person, period, parameters): + def formula_2018(person, period, parameters): allowances = person("allowances", period) ani = person("net_income", period) rates = parameters(period).gov.hmrc.income_tax.rates - scot = person("pays_scottish_income_tax", period) income = ani - allowances gross_gift = person("gift_aid_grossed_up", period) # s.414(2)(b) extends the limits used to determine the tax band. @@ -84,18 +157,8 @@ def formula_2018_06_01(person, period, parameters): [TaxBand.NONE, TaxBand.BASIC, TaxBand.HIGHER], default=TaxBand.ADDITIONAL, ) - scottish_band = select( - [ - income < threshold + (gross_gift if index >= 2 else 0) - for index, threshold in enumerate(rates.scotland.rates.thresholds[:5]) - ], - [ - TaxBand.NONE, - TaxBand.STARTER, - TaxBand.BASIC, - TaxBand.INTERMEDIATE, - TaxBand.HIGHER, - ], - default=TaxBand.ADDITIONAL, + return where( + person("pays_scottish_income_tax", period), + scottish_tax_band(person, period, parameters), + uk_band, ) - return where(scot, scottish_band, uk_band) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/_earnings.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/_earnings.py index dca397211a..640c6ff413 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/_earnings.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/_earnings.py @@ -1,5 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.class_2 import class_2_contribution_weeks, class_2_liable +from policyengine_uk.utils.uc_income_tax import earnings_tax_reductions from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_wales_scheme, ) @@ -169,20 +170,23 @@ def working_age_earnings(person, period, parameters): return net, net_employed -def working_age_unearned_income_tax(person, period): +def working_age_unearned_income_tax(person, period, parameters): """Income tax on the person's counted taxable unearned income. earned_income_tax is the tax on non-savings income other than property - income: earnings, pensions and taxable social security benefits. Less the - tax on earnings (uc_income_tax_on_earnings, earnings being the lowest - slice), the rest is tax on pensions and benefits, shared pro rata with the - taxable ones the schemes do not count (the Scottish Carer Supplement). - The tax on miscellaneous income, which the schemes count as unearned - income, is added (_pay_and_miscellaneous_tax). + income, before the Step 6 tax reductions: earnings, pensions and taxable + social security benefits. uc_income_tax_on_earnings (earnings being the + lowest slice) bears those reductions first, so the formula takes them off + earned_income_tax too. Less the tax on earnings, the rest is tax on + pensions and benefits, shared pro rata with the taxable ones the schemes + do not count (the Scottish Carer Supplement). The tax on miscellaneous + income, which the schemes count as unearned income, is added + (_pay_and_miscellaneous_tax). """ other_non_savings_tax = max_( 0, person("earned_income_tax", period) + - earnings_tax_reductions(person, period, parameters) - person("uc_income_tax_on_earnings", period), ) base = add( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/council_tax_reduction_working_age_unearned_income_tax.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/council_tax_reduction_working_age_unearned_income_tax.py index 3306f7986d..a9dbd78401 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/council_tax_reduction_working_age_unearned_income_tax.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/working_age/council_tax_reduction_working_age_unearned_income_tax.py @@ -15,8 +15,9 @@ class council_tax_reduction_working_age_unearned_income_tax(Variable): "Income tax of the applicant and partner on the taxable unearned " "income the working-age schemes count (pensions, State Pension and " "taxable benefits): their non-savings income tax other than on " - "property income (earned_income_tax) less the tax on their earnings, " - "shared pro rata with taxable income the schemes do not count (the " + "property income (earned_income_tax), less the Step 6 tax reductions " + "that come off the tax on earnings first and less the tax on their " + "earnings, shared pro rata with taxable income the schemes do not count (the " "Scottish Carer Supplement). Wales disregards it from unearned income " "(WSI 2013/3029 Sch 9 para 4); Scotland counts unearned income gross " "(SSI 2021/249 reg 57)." @@ -27,5 +28,5 @@ class council_tax_reduction_working_age_unearned_income_tax(Variable): def formula(benunit, period, parameters): person = benunit.members - tax = working_age_unearned_income_tax(person, period) + tax = working_age_unearned_income_tax(person, period, parameters) return benunit.sum(working_age_applicant_or_partner(person, period) * tax)