From 705b3d746b633c294c0521cb02c2503119d44268 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 18:42:06 -0400 Subject: [PATCH 01/33] Band Scottish taxpayers' savings and dividends on rest-of-UK limits tax_band compared a Scottish taxpayer's total taxable income, savings and dividends included, with the Scottish limits. The Scottish rates charge only income that is neither savings nor dividend income (ITA 2007 s. 11A), and savings and dividends take the rest-of-UK bands (ss. 11D(6), 13(5)). The band is now the higher of the Scottish band of non-savings income and, where any taxable income is savings or dividends, the rest-of-UK band of all taxable income. Income at a limit stays in the lower band. The same formula now runs from 2017-18: before, 2017-18 used the rest-of-UK limits for Scottish taxpayers and 2018-19 read the five-rate Scottish schedule as the three-rate one. Other taxpayers are unchanged. Fixes #2350 Co-Authored-By: Claude Opus 5.5 --- .../bracketized_liability/tax_band.py | 181 ++++++++++++------ 1 file changed, 120 insertions(+), 61 deletions(-) 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..2aa4512393 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,137 @@ class TaxBand(Enum): ADDITIONAL = "Additional" +def uk_tax_band_above_limits(person, period, parameters): + """The rest-of-UK band before 2018-19: income above a limit is in the + band above it.""" + 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. + + The Scottish rates charge only income that is neither savings nor + dividend income (ITA 2007 s. 11A and the Scottish rate resolutions), so + that income is banded on the Scottish limits. Savings and dividends are + the top slice of income (s. 16) and are charged at the rates they would + bear if the person were not a Scottish taxpayer (ss. 11D(6) and 13(5)), + so where any taxable income is savings or dividends, it is banded on + the rest-of-UK limits at the top of all taxable income. The band is the + higher of the two. Income up to a limit is charged at the lower rate + (s. 10(2) to (3A); each resolution charges a rate "on income above" one + limit "and up to" the next), so income at a limit stays in the lower + band. + """ + 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. Marriage + # Allowance is a tax reduction (s. 55B(1)), so it does not move the band. + non_savings = person("non_savings_income_after_allowances", period) + # s. 414(2)(b) raises the Scottish basic rate's upper limit and every + # limit above it by the grossed-up gift. + 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) + # Taxable income above the non-savings income is savings or dividends. + # s. 414(2)(b) raises the basic and higher rate limits by the gift too. + 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. A " + "Scottish taxpayer's band is 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, the + # first year of Scottish rates and 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 +153,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) From 92be9ef15584f832f5c39390bbc6a0887fd1bb9d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 18:55:04 -0400 Subject: [PATCH 02/33] Test tax_band for Scottish savings, dividends and limits Cases with hand-worked arithmetic for the issue's Scottish incomes with dividends and with savings, both limits at once, one pound above the basic rate limit, Gift Aid, allowances beyond pay, starter-rate pay, 2017-18 and 2018-19, with rest-of-UK controls. Eleven fail on main. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2350.fixed.md | 1 + .../tax/income_tax/scottish_rates.yaml | 378 ++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 changelog.d/2350.fixed.md diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md new file mode 100644 index 0000000000..efb925e014 --- /dev/null +++ b/changelog.d/2350.fixed.md @@ -0,0 +1 @@ +`tax_band` now bands a Scottish taxpayer's income other than savings and dividends on the Scottish limits and their savings and dividends on the rest-of-UK limits, from 2017-18, so the Marriage Allowance conditions and counts by marginal rate no longer put them in a band too high. 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..d1f3520d2d 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,381 @@ earned_taxable_income: 130000 earned_income_tax: 50034.35 income_tax: 50034.35 + +# tax_band for a Scottish taxpayer with savings or dividends (#2350). The +# Scottish rates charge only income that is neither savings nor dividends +# (ITA 2007 s. 11A(1A); S6M-16531 for 2025-26). Savings and dividends are the +# top slice (s. 16) and take the rates and bands they would bear outside +# Scotland (ss. 11D(6) and 13(5)). The band is the higher of the two. In +# 2025-26 the personal allowance is £12,570, the Scottish limits on taxable +# income are £2,827, £14,921, £31,092, £62,430 and £125,140 (19%, 20%, 21%, +# 42%, 45%, 48%), the basic rate limit is £37,700, the dividend allowance is +# £500 (8.75% ordinary, 33.75% upper) and the Personal Savings Allowance is +# £1,000, or £500 with higher-rate income (s. 12B). Marriage Allowance needs +# no liability above the Scottish intermediate or dividend ordinary rate +# (s. 55B(2)(b)). + +- name: Scottish 2025 pay of 40000 and dividends of 5000 is intermediate + 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 is intermediate + 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 starting rate for + # savings is used up by 27,430 of other income (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 is basic + 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 is higher + 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 is higher + 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 is higher + 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 are higher + 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 are basic + 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 are basic + 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 is starter + 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. Nothing is 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 is intermediate + 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 is higher + 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. The 2017-18 s. 55B(2)(b) + # allows the Scottish basic rate but not the Scottish higher rate. + tax_band: HIGHER + meets_marriage_allowance_income_conditions: false + earned_income_tax: 6_700 From a2d64eee674736d8f97491bae131788793a4076b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 19:55:12 -0400 Subject: [PATCH 03/33] Test tax_band for Scottish taxpayers in 2026-27 Co-Authored-By: Claude Opus 5.5 --- .../tax/income_tax/scottish_rates.yaml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) 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 d1f3520d2d..c881f6947d 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 @@ -505,3 +505,98 @@ 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 is intermediate + 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 is higher + 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 are higher + 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 are basic + period: 2026 + input: + people: + person: + age: 40 + employment_income: 5_000 + dividend_income: 30_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # 22,430 of dividends remain after the allowance, below 37,700, and no + # income is charged at the Scottish rates. + tax_band: BASIC + meets_marriage_allowance_income_conditions: true From c8139c24727ba597e51c063e35dd9840f8de5de7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 20:25:16 -0400 Subject: [PATCH 04/33] Add property tests reading tax_band off the liability For Scottish taxpayers the tests raise one band's rates at a time and check that a person in a band pays tax at that band's rates and at no higher band's, that the Marriage Allowance conditions match, and that against the formula before #2350 the band rises only from starter to basic. They also show savings_allowance does not read tax_band. Co-Authored-By: Claude Opus 5.5 --- .../test_tax_band_scottish_properties.py | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 policyengine_uk/tests/test_tax_band_scottish_properties.py 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..5df4c0be6c --- /dev/null +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -0,0 +1,302 @@ +"""Property tests for a Scottish taxpayer's tax_band (#2350). + +tax_band is 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. + +With the starting rate for savings, the Personal Savings Allowance and the +dividend allowance set to nothing, every pound of taxable income is charged at +a band's rate, and for every 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)). + +Under the law in force the nil rates charge some income at 0% where it sits, +so the band can rest on that income alone, but no one pays tax at the higher +or additional rates above their band. + +Before #2350 the band compared all taxable income with the Scottish limits. +From 2019-20, when that formula last applied, the band never rises, except +from STARTER to BASIC where savings or dividends sit below the Scottish +starter limit: they are charged at the rest-of-UK basic rates, not at the +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 raises the tax by the income +# charged at them. Taxable income comes in multiples of 50p (the personal +# allowance taper halves pounds), so SOME separates any income in a band from +# 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" +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(sim, period, bands): + """The income charged at each band's rates: the rise in tax when only + that band's rates rise by RISE, divided by RISE.""" + parameters = sim.tax_benefit_system.parameters + before = liability(sim, period) + income_at = {} + for band in bands: + rates = { + path: parameters.get_child(path)(str(period)) for path in BAND_RATES[band] + } + # A rate with no value that year (a Scottish rate not yet set) stays so. + raised = {path: rate + RISE for path, rate in rates.items() if rate is not None} + with law(sim, period, raised): + income_at[band] = (liability(sim, period) - before) / RISE + return income_at + + +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) + # The Marriage Allowance and counts by marginal rate turn on these. + 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 are basic, not starter. + 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(): + # ITA 2007 s. 12B(8) sets the Personal Savings Allowance on the + # rest-of-UK bands for everyone, 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 + ) From 174a9b2382e06e459b46b2d2e3d35aee08ee63c6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 20:27:00 -0400 Subject: [PATCH 05/33] Correct the property tests' note on the earlier formula Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/test_tax_band_scottish_properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 5df4c0be6c..271190d5b8 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -22,7 +22,7 @@ or additional rates above their band. Before #2350 the band compared all taxable income with the Scottish limits. -From 2019-20, when that formula last applied, the band never rises, except +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 are charged at the rest-of-UK basic rates, not at the starter rate. From 1983fd25e917b4290469dcbd93c3c0917d349cb2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 21:07:12 -0400 Subject: [PATCH 06/33] Move the Gift Aid snapshot's Scottish mixed-income band to basic The tenth snapshot row is a Scottish taxpayer with no pay and 10,000 of property income, 20,000 of savings and 10,000 of dividends. The allowance takes all the property income, so nothing is charged at a Scottish rate (ITA 2007 s. 11A), and the 27,430 of savings and dividends left sit below the basic rate limit (ss. 11D(6) and 13(5)). The band is basic. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/test_gift_aid.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_gift_aid.py b/policyengine_uk/tests/test_gift_aid.py index 34d7bb182c..621d1e062c 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 allowance takes all 10,000 of property income, so nothing is + # charged at a Scottish rate (ITA 2007 s. 11A). The 27,430 of + # savings and dividends left sit below the 37,700 basic rate limit + # (ss. 11D(6) and 13(5)), so the band is basic, not intermediate + # (#2350). + "BASIC", "ADDITIONAL", ] From db76188bb242951a19213790cca1360e4f15bc27 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 21:40:15 -0400 Subject: [PATCH 07/33] Scope the band notes and name tax_band as a reader Co-Authored-By: Claude Opus 5.5 --- changelog.d/2350.fixed.md | 2 +- .../tests/test_tax_band_scottish_properties.py | 17 ++++++++++------- .../non_savings_income_after_allowances.py | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md index efb925e014..7d9d305281 100644 --- a/changelog.d/2350.fixed.md +++ b/changelog.d/2350.fixed.md @@ -1 +1 @@ -`tax_band` now bands a Scottish taxpayer's income other than savings and dividends on the Scottish limits and their savings and dividends on the rest-of-UK limits, from 2017-18, so the Marriage Allowance conditions and counts by marginal rate no longer put them in a band too high. +`tax_band` now bands a Scottish taxpayer's income other than savings and dividends on the Scottish limits and their savings and dividends on the rest-of-UK limits, from 2017-18, so the Marriage Allowance conditions and counts by marginal rate follow the rates at which their income is charged. diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 271190d5b8..c476d99417 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -8,9 +8,11 @@ 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. +The people are single, so the Marriage Allowance, which the model takes off +earned income rather than giving as a tax reduction (#1947), plays no part. With the starting rate for savings, the Personal Savings Allowance and the dividend allowance set to nothing, every pound of taxable income is charged at -a band's rate, and for every Scottish taxpayer: +a band's 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; @@ -24,8 +26,9 @@ 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 are charged at the rest-of-UK basic rates, not at the -starter rate. +starter limit: they sit in the rest-of-UK basic band (charged at the starting +rate for savings, the dividend nil rate or the dividend ordinary rate), not at +the Scottish starter rate. """ from contextlib import contextmanager @@ -44,9 +47,9 @@ YEARS = list(range(2017, 2027)) PEOPLE = 16 # Raising a band's rates by 100 percentage points raises the tax by the income -# charged at them. Taxable income comes in multiples of 50p (the personal -# allowance taper halves pounds), so SOME separates any income in a band from -# float32 rounding. +# charged at them. Taxable income comes in whole pounds (the tapered personal +# allowance is rounded up to a pound), so SOME separates any income in a band +# from float32 rounding. RISE = 1.0 SOME = 0.25 BANDS = ["NONE", "STARTER", "BASIC", "INTERMEDIATE", "HIGHER", "ADDITIONAL"] @@ -280,7 +283,7 @@ def test_band_never_rises_except_from_starter_to_basic(sim, drawn, period): for i in range(PEOPLE): if RANK[band[i]] <= RANK[before[i]]: continue - # Savings or dividends below the starter limit are basic, not starter. + # 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 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..d8485dbc20 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 @@ -11,8 +11,8 @@ class non_savings_income_after_allowances(Variable): "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." + "property_income_tax, taxable_property_income_after_allowances and, " + "for Scottish taxpayers, tax_band read it." ) definition_period = YEAR reference = dict( From 4c011bc9164cdfa7ee14e0b6d4c6858bdc8ac8cd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:37:58 -0400 Subject: [PATCH 08/33] Apply the s. 55B rate conditions to income for the Marriage Allowance meets_marriage_allowance_income_conditions stops reading tax_band. It ports liable_only_at_marriage_allowance_rates from #1963 and computes it from income before the Marriage Allowance, since this head still takes the allowance off earned_taxable_income: non-savings income up to the start of the Scottish higher rate or the basic rate limit, savings above the limit only where the savings nil rate and the starting rate for savings cover them, and dividends counted without the dividend allowance (s. 55B(2)(ba)). The CPS expanded reform reads the same helper, and the eligible_bands parameter, which nothing else read, goes. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2350.fixed.md | 2 +- .../marriage_allowance/eligible_bands.yaml | 14 -- .../reforms/cps/marriage_tax_reforms.py | 17 +- .../tax/income_tax/scottish_rates.yaml | 103 +++++++- ..._marriage_allowance_income_conditions.yaml | 230 +++++++++++++++++- policyengine_uk/tests/test_gift_aid.py | 10 +- .../test_marriage_allowance_cps_switch.py | 40 ++- ...iage_allowance_eligible_bands_parameter.py | 36 +++ .../test_tax_band_scottish_properties.py | 149 ++++++++++-- policyengine_uk/utils/parameters.py | 9 + ...ts_marriage_allowance_income_conditions.py | 105 +++++++- .../bracketized_liability/tax_band.py | 23 +- 12 files changed, 638 insertions(+), 100 deletions(-) delete mode 100644 policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml create mode 100644 policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md index 7d9d305281..d9124170c2 100644 --- a/changelog.d/2350.fixed.md +++ b/changelog.d/2350.fixed.md @@ -1 +1 @@ -`tax_band` now bands a Scottish taxpayer's income other than savings and dividends on the Scottish limits and their savings and dividends on the rest-of-UK limits, from 2017-18, so the Marriage Allowance conditions and counts by marginal rate follow the rates at which their income is charged. +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 Marriage Allowance eligibility applies the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to income directly, so income at a rate limit and savings at the savings nil rate no longer stop it. 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/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index 16ddaa7960..943c237bb8 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 @@ -41,16 +44,12 @@ class meets_marriage_allowance_income_conditions(Variable): 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 if the reform's + # expansion conditions are met. + 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/tax/income_tax/scottish_rates.yaml b/policyengine_uk/tests/policy/baseline/finance/tax/income_tax/scottish_rates.yaml index c881f6947d..79a99c54e6 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 @@ -137,9 +137,9 @@ # income are £2,827, £14,921, £31,092, £62,430 and £125,140 (19%, 20%, 21%, # 42%, 45%, 48%), the basic rate limit is £37,700, the dividend allowance is # £500 (8.75% ordinary, 33.75% upper) and the Personal Savings Allowance is -# £1,000, or £500 with higher-rate income (s. 12B). Marriage Allowance needs -# no liability above the Scottish intermediate or dividend ordinary rate -# (s. 55B(2)(b)). +# £1,000, or £500 with higher-rate income (s. 12B). Marriage Allowance +# applies the rate conditions of s. 55B(2)(b) and the dividend counterfactual +# of s. 55B(2)(ba); savings charged at the savings nil rate leave it open. - name: Scottish 2025 pay of 40000 and dividends of 5000 is intermediate period: 2025 @@ -178,8 +178,9 @@ 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 starting rate for - # savings is used up by 27,430 of other income (s. 12). + # 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 @@ -596,7 +597,95 @@ members: [person] country: SCOTLAND output: - # 22,430 of dividends remain after the allowance, below 37,700, and no - # income is charged at the Scottish rates. + # 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: + # Income of 125,140 withdraws the whole personal allowance (s. 35), so + # all 125,140 is taxable, up to the 125,140 higher rate limit and not + # above it. With no other income, 5,000 is at the starting rate for + # savings (s. 12) and the higher-rate allowance covers 500 (s. 12B); the + # rest of the 37,700 is at 20% and the 87,440 above it at 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: 32_092 + households: + household: + members: [person] + country: SCOTLAND + output: + # The 1,000 property allowance leaves 31,092 of property income, and 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 is higher + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_570 + property_income: 32_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/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..7fa7fca824 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,247 @@ -- 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 the personal +# allowance is £12,570 and the basic rate limit £37,700, and the Scottish +# higher rate starts at £31,092 of taxable income (S6M-16531 for 2025-26, +# S6M-20844 for 2026-27). In 2026-27 the dividend allowance is £500 and the +# Personal Savings Allowance £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, so 9,730 is 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 + # No personal allowance above 125,140 of income; 10,000 is above 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: Scottish higher-rate threshold reform keeps nil-rate savings eligible + 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 the Personal Savings + # Allowance is 500 (s. 12B) and charges it at the savings nil rate + # (s. 12A), which s. 55B(2)(b) allows. The band rests on that pound. + 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: + # As above with 501 of interest: 1 is at the savings higher rate, 40p. + employment_income: 50_070 + 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: Taxable income at the basic rate limit meets the conditions + period: 2026 + input: + # 50,270 - 12,570 = 37,700: s. 10(2) charges income up to the basic rate + # limit at the basic rate. + 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: S6M-20844 charges the intermediate rate on + # income up to 31,092 and the higher rate only above it. + 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 be at 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 be at 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 2017-18 the Scottish higher rate fails the conditions + period: 2017 + input: + # The 2017-18 s. 55B(2)(b) allows the Scottish basic rate only. 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: + # SI 2018/459 art. 6(2) adds the Scottish starter and intermediate rates + # to s. 55B(2)(b). 40,000 - 11,850 = 28,150, at the intermediate rate. + employment_income: 40_000 + region: SCOTLAND output: meets_marriage_allowance_income_conditions: true diff --git a/policyengine_uk/tests/test_gift_aid.py b/policyengine_uk/tests/test_gift_aid.py index 621d1e062c..16df5f01dc 100644 --- a/policyengine_uk/tests/test_gift_aid.py +++ b/policyengine_uk/tests/test_gift_aid.py @@ -371,11 +371,11 @@ def test_zero_gift_preserves_baseline_outputs_bit_for_bit(): "HIGHER", "ADDITIONAL", "HIGHER", - # The allowance takes all 10,000 of property income, so nothing is - # charged at a Scottish rate (ITA 2007 s. 11A). The 27,430 of - # savings and dividends left sit below the 37,700 basic rate limit - # (ss. 11D(6) and 13(5)), so the band is basic, not intermediate - # (#2350). + # 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 the band is basic, + # not intermediate (#2350). "BASIC", "ADDITIONAL", ] diff --git a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py index caeacd7ce2..5c40b29c71 100644 --- a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py +++ b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py @@ -24,9 +24,10 @@ 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). @@ -69,6 +70,12 @@ # (marriage_allowance/rounding_increment.yaml). PERSONAL_ALLOWANCE = 12_570 ROUNDING_INCREMENT = 10 +# 2025-26 taxable income above which non-savings income is charged above the +# rates that allow 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. @@ -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, whose starter and intermediate rates +# allow the allowance (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 all their taxable + # income is non-savings 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_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..20d308d795 --- /dev/null +++ b/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py @@ -0,0 +1,36 @@ +"""Saved policies naming the Marriage Allowance eligible bands get directions. + +The income conditions apply the rate conditions of ITA 2007 s. 55B(2)(b) and +(ba) directly, so nothing reads +``gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands`` (#2350). +""" + +import pytest + +from policyengine_uk import Simulation +from policyengine_uk.system import system +from policyengine_uk.utils.scenario import Scenario + +REMOVED = "gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands" + + +@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 + + +def test_removed_eligible_bands_give_directions(): + with pytest.raises(ValueError) as error: + Simulation( + situation={"people": {"person": {"age": {2025: 40}}}}, + scenario=Scenario(parameter_changes={REMOVED: {"2025": ["BASIC"]}}), + ) + 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 diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index c476d99417..9b75edd351 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -1,4 +1,5 @@ -"""Property tests for a Scottish taxpayer's tax_band (#2350). +"""Property tests for a Scottish taxpayer's tax_band and the Marriage +Allowance income conditions (#2350). tax_band is the highest band in which a person has taxable income. For a Scottish taxpayer the liability charges income other than savings and @@ -19,9 +20,17 @@ - the Marriage Allowance income conditions hold exactly when the person pays nothing at the higher or additional rates (s. 55B(2)(b)). -Under the law in force the nil rates charge some income at 0% where it sits, -so the band can rest on that income alone, but no one pays tax at the higher -or additional rates above their band. +With baseline nil rates restored, 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 nil +rates in force, under the law in force or with its rate limits and nil-rate +amounts changed, for every single person in Scotland or England 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. 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 @@ -46,15 +55,16 @@ # 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 raises the tax by the income -# charged at them. Taxable income comes in whole pounds (the tapered personal -# allowance is rounded up to a pound), so SOME separates any income in a band -# from float32 rounding. +# 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, @@ -181,21 +191,21 @@ def liability(sim, period): return sum(sim.calculate(v, period).astype(float) for v in LIABILITY) -def charged(sim, period, bands): - """The income charged at each band's rates: the rise in tax when only - that band's rates rise by RISE, divided by RISE.""" +def charged_at(sim, period, paths): + """The income charged at the rates at these paths: the rise in tax when + only those rates rise by RISE, divided by RISE.""" parameters = sim.tax_benefit_system.parameters before = liability(sim, period) - income_at = {} - for band in bands: - rates = { - path: parameters.get_child(path)(str(period)) for path in BAND_RATES[band] - } - # A rate with no value that year (a Scottish rate not yet set) stays so. - raised = {path: rate + RISE for path, rate in rates.items() if rate is not None} - with law(sim, period, raised): - income_at[band] = (liability(sim, period) - before) / RISE - return income_at + rates = {path: parameters.get_child(path)(str(period)) for path in paths} + # A rate with no value that year (a Scottish rate not yet set) stays so. + 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): @@ -303,3 +313,100 @@ def test_savings_allowance_does_not_read_the_band(): np.testing.assert_array_equal( sim.calculate("savings_allowance", 2025), allowance ) + + +# The Marriage Allowance income conditions against the liability, with the nil +# rates in force: people alternate between Scotland and England, from 2016-17, +# the first year of the savings and dividend nil rates. +MA_YEARS = list(range(2016, 2027)) +MA_COUNTRIES = ["SCOTLAND", "ENGLAND"] +# The rates s. 55B(2)(b) forbids, 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" +# 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 + ), + "gov.hmrc.income_tax.rates.uk[1].threshold": st.integers(25_000, 50_000), + "gov.hmrc.income_tax.rates.dividends[1].threshold": st.integers(25_000, 50_000), + "gov.hmrc.income_tax.rates.savings_starter_rate.allowance": 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_nil_rates_never_stop_the_marriage_allowance(mixed_sim, drawn, period, changes): + sim = mixed_sim + set_people(sim, drawn, period) + parameters = sim.tax_benefit_system.parameters + # A limit with no value that year (a Scottish rate not yet set) stays so. + changes = { + path: value + for path, value in changes.items() + if parameters.get_child(path)(str(period)) is not None + } + 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) + np.testing.assert_array_equal( + meets, + (forbidden < SOME) & (dividends_above_basic < SOME), + err_msg=f"{drawn} in {period} with {changes}", + ) diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index 2e408f7ee4..e4f8682274 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 Marriage Allowance income conditions no longer read tax_band. " + "They apply the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to " + "the person's income directly, so they follow the rate limits: 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/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..ecd663d3a2 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,107 @@ 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. + + ITA 2007 s. 55B(2)(b) and s. 55C(1)(c) allow only the basic rate, the + property, default, savings, Scottish and Welsh basic rates, the Scottish + starter and intermediate rates, the dividend ordinary rate and the nil and + starting rates for savings. So no non-savings income may reach the higher + rate (the Scottish higher rate for a Scottish taxpayer), and no savings the + savings higher rate. Dividends covered by the dividend nil rate still + count: s. 55B(2)(ba) and s. 55C(1)(ca) test the dividend upper rate as if + s. 13A were omitted. + + The bands are the ones the model taxes with: Gift Aid extends the rate + limits by the grossed-up gift (s. 414(2)(b)), as in earned_income_tax, + and does not come off the income. + + The test reads income before the Marriage Allowance. The rates a person + is liable at come from Step 3 of s. 23, before the tax reductions of + Step 6, of which the allowance is one (s. 55B(1)). The model still takes + the allowance off earned_taxable_income (#1947), so + non_savings_taxable_income and the savings rate bands built on it would + read this test back. + """ + 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). It is non_savings_taxable_income before the + # Marriage Allowance. + 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 Scottish rates used the UK limits. + scottish = rates.scotland.rates + basic_rate_limit = rates.uk.thresholds[1] + gross_gift + if len(scottish.rates) > 3: + scottish_limit = scottish.thresholds[3] + gross_gift + elif len(scottish.rates) == 3: + scottish_limit = scottish.thresholds[1] + gross_gift + else: + scottish_limit = basic_rate_limit + 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 + # bands (s. 11D(6)). The starting rate for savings and then the savings + # nil rate take the first of them, as in higher_rate_savings_income; the + # model keeps the starting rate within the basic rate band. Savings above + # those are charged at a positive rate, and fail the test once they pass + # the basic rate limit. + savings = max_( + 0, + person("taxable_savings_interest_income", period) + - person("received_allowances_savings_income", period), + ) + starter = rates.savings_starter_rate + savings_in_basic_band = clip( + non_savings + savings, rates.uk.thresholds[0], basic_rate_limit + ) - clip(non_savings, rates.uk.thresholds[0], basic_rate_limit) + starting_rate_savings = clip( + min_(savings, savings_in_basic_band), + 0, + max_(0, starter.allowance - non_savings * starter.phase_out), + ) + nil_rate_savings = starting_rate_savings + person("savings_allowance", period) + savings_above_basic = (savings > nil_rate_savings) & ( + non_savings + savings > basic_rate_limit + ) + # Dividends are the top slice, above all non-savings income and savings + # (s. 16), counted before the dividend allowance. The dividend upper rate + # starts at the basic rate limit (s. 13(2)), extended by the gross gift; + # dividend_income_tax reads that limit from the dividend scale. + dividends = max_( + 0, + person("taxable_dividend_income", period) + - person("received_allowances_dividend_income", period), + ) + dividend_upper_limit = rates.dividends.thresholds[1] + gross_gift + dividends_above_basic = (dividends > 0) & ( + non_savings + savings + dividends > dividend_upper_limit + ) + return (non_savings <= non_savings_limit) & ~savings_above_basic & ~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 the rates that let them " + "receive 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/bracketized_liability/tax_band.py b/policyengine_uk/variables/gov/hmrc/income_tax/bracketized_liability/tax_band.py index 2aa4512393..8d3df164b2 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 @@ -35,17 +35,18 @@ def uk_tax_band_above_limits(person, period, parameters): def scottish_tax_band(person, period, parameters): """The highest band in which a Scottish taxpayer has taxable income. - The Scottish rates charge only income that is neither savings nor - dividend income (ITA 2007 s. 11A and the Scottish rate resolutions), so - that income is banded on the Scottish limits. Savings and dividends are - the top slice of income (s. 16) and are charged at the rates they would - bear if the person were not a Scottish taxpayer (ss. 11D(6) and 13(5)), - so where any taxable income is savings or dividends, it is banded on - the rest-of-UK limits at the top of all taxable income. The band is the - higher of the two. Income up to a limit is charged at the lower rate - (s. 10(2) to (3A); each resolution charges a rate "on income above" one - limit "and up to" the next), so income at a limit stays in the lower - band. + Use the Scottish limits for income other than savings and dividends, + the only income the Scottish rates charge (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: s. 10(2) to (3A) charges income "up to" a limit at the + lower rate, and each resolution charges a rate "on income above" one + limit "and up to" the next. As for every taxpayer, income charged at a + nil rate stays in the band where it sits; the Marriage Allowance applies + its own rate conditions (liable_only_at_marriage_allowance_rates). """ rates = parameters(period).gov.hmrc.income_tax.rates gross_gift = person("gift_aid_grossed_up", period) From 319f197e2c2c3e4ed0c47481cf750f957a2c123e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:59:17 -0500 Subject: [PATCH 09/33] Wrap the Marriage Allowance rate test's result for the line limit Co-Authored-By: Claude Opus 5.5 --- .../meets_marriage_allowance_income_conditions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 ecd663d3a2..b84131e703 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 @@ -84,7 +84,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): dividends_above_basic = (dividends > 0) & ( non_savings + savings + dividends > dividend_upper_limit ) - return (non_savings <= non_savings_limit) & ~savings_above_basic & ~dividends_above_basic + return ( + (non_savings <= non_savings_limit) + & ~savings_above_basic + & ~dividends_above_basic + ) class meets_marriage_allowance_income_conditions(Variable): From d73ecfb4241ba8db6cc2b9b3a2f440e5739ac71b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 04:11:21 -0500 Subject: [PATCH 10/33] Cite Step 4 of s. 23 and name who acts in the rate-test notes Co-Authored-By: Claude Opus 5.5 --- ..._marriage_allowance_income_conditions.yaml | 3 ++- ...ts_marriage_allowance_income_conditions.py | 19 ++++++++++--------- .../bracketized_liability/tax_band.py | 6 +++--- 3 files changed, 15 insertions(+), 13 deletions(-) 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 7fa7fca824..e8d792b6ea 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 @@ -70,7 +70,8 @@ # 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 the Personal Savings # Allowance is 500 (s. 12B) and charges it at the savings nil rate - # (s. 12A), which s. 55B(2)(b) allows. The band rests on that pound. + # (s. 12A), which s. 55B(2)(b) allows. 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 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 b84131e703..8458f00c17 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 @@ -17,9 +17,9 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): limits by the grossed-up gift (s. 414(2)(b)), as in earned_income_tax, and does not come off the income. - The test reads income before the Marriage Allowance. The rates a person - is liable at come from Step 3 of s. 23, before the tax reductions of - Step 6, of which the allowance is one (s. 55B(1)). The model still takes + The test reads income before the Marriage Allowance. Step 4 of s. 23 + charges tax at each rate before Step 6 deducts the tax reductions, and + s. 55B(1) gives the allowance as a tax reduction. The model still takes the allowance off earned_taxable_income (#1947), so non_savings_taxable_income and the savings rate bands built on it would read this test back. @@ -33,7 +33,8 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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 Scottish rates used the UK limits. + # The model holds no Scottish scale before 2017-18, and in 2016-17 + # s. 11A(1) charged the Scottish basic rate up to the basic rate limit. scottish = rates.scotland.rates basic_rate_limit = rates.uk.thresholds[1] + gross_gift if len(scottish.rates) > 3: @@ -48,11 +49,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): basic_rate_limit, ) # Savings sit above non-savings income (s. 16) and take the rest-of-UK - # bands (s. 11D(6)). The starting rate for savings and then the savings - # nil rate take the first of them, as in higher_rate_savings_income; the - # model keeps the starting rate within the basic rate band. Savings above - # those are charged at a positive rate, and fail the test once they pass - # the basic rate limit. + # bands (s. 11D(6)). As in higher_rate_savings_income, the model charges + # the first of them at the starting rate for savings, which it keeps + # within the basic rate band, and then at the savings nil rate. Savings + # above those bear a positive rate, and the person fails the test once + # they pass the basic rate limit. savings = max_( 0, person("taxable_savings_interest_income", period) 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 8d3df164b2..457cd7f9c7 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 @@ -44,9 +44,9 @@ def scottish_tax_band(person, period, parameters): remain, then return the higher band. Keep income at each upper limit in the lower band: s. 10(2) to (3A) charges income "up to" a limit at the lower rate, and each resolution charges a rate "on income above" one - limit "and up to" the next. As for every taxpayer, income charged at a - nil rate stays in the band where it sits; the Marriage Allowance applies - its own rate conditions (liable_only_at_marriage_allowance_rates). + limit "and up to" the next. 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) From 3fdf2368d5a07d374a91064bd7ad1c4e257ee46f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 04:14:55 -0500 Subject: [PATCH 11/33] Name the actor in the rate-test labels and the changelog Co-Authored-By: Claude Opus 5.5 --- changelog.d/2350.fixed.md | 2 +- .../meets_marriage_allowance_income_conditions.yaml | 2 +- policyengine_uk/tests/test_tax_band_scottish_properties.py | 4 +++- policyengine_uk/utils/parameters.py | 6 +++--- .../meets_marriage_allowance_income_conditions.py | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md index d9124170c2..9f9b9e9129 100644 --- a/changelog.d/2350.fixed.md +++ b/changelog.d/2350.fixed.md @@ -1 +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 Marriage Allowance eligibility applies the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to income directly, so income at a rate limit and savings at the savings nil rate no longer stop it. +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. It now tests Marriage Allowance eligibility against the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) directly, so income at a rate limit and savings at the savings nil rate no longer block it. 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 e8d792b6ea..6203a66726 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 @@ -63,7 +63,7 @@ output: meets_marriage_allowance_income_conditions: true -- name: Scottish higher-rate threshold reform keeps nil-rate savings eligible +- 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 diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 9b75edd351..9ab2ceb4a9 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -388,7 +388,9 @@ def mixed_sim(): @given(people, st.sampled_from(MA_YEARS), limits) @example(NIL_RATE, 2025, WITNESS_LIMITS) @example(NIL_RATE, 2025, {}) -def test_nil_rates_never_stop_the_marriage_allowance(mixed_sim, drawn, period, changes): +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 diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index e4f8682274..1b1aa8f821 100644 --- a/policyengine_uk/utils/parameters.py +++ b/policyengine_uk/utils/parameters.py @@ -256,9 +256,9 @@ class RemovedParameter(NamedTuple): ), "gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands": RemovedParameter( "removed Marriage Allowance eligible tax bands", - "The Marriage Allowance income conditions no longer read tax_band. " - "They apply the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) to " - "the person's income directly, so they follow the rate limits: change " + "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/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 8458f00c17..37e214300a 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 @@ -95,8 +95,8 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): class meets_marriage_allowance_income_conditions(Variable): label = "Meets Marriage Allowance income conditions" documentation = ( - "Whether this person pays income tax only at the rates that let them " - "receive the Marriage Allowance (ITA 2007 s. 55B(2)(b) and (ba))." + "Whether this person pays income tax only at rates that ITA 2007 " + "s. 55B(2)(b) and (ba) allow for receiving the Marriage Allowance." ) entity = Person definition_period = YEAR From 53cfaf4b11e33b8e667e2e61999863e7d1fd6baa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 04:49:49 -0500 Subject: [PATCH 12/33] Use the basic rate limit for Scots in the rate test before 2017-18 The model's Scottish scale carries its 2017-18 values back to 2015-16 and 2016-17, so the rate test's empty-scale branch never ran and it read the 31,500 Scottish higher-rate start for those years. In 2016-17 s. 11A(1) charged the Scottish basic rate on income that would otherwise bear the basic rate, and 2015-16 had no Scottish rate, so the test now uses the basic rate limit before 2017-18. The property test leaves Scottish taxpayers out before 2017-18, where the model's liability uses the carried-back scale, and Ruff formats its limits table. Co-Authored-By: Claude Opus 5.5 --- ..._marriage_allowance_income_conditions.yaml | 24 ++++++++++++++ .../test_tax_band_scottish_properties.py | 32 ++++++++++++------- ...ts_marriage_allowance_income_conditions.py | 13 +++++--- 3 files changed, 53 insertions(+), 16 deletions(-) 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 6203a66726..32f9daff2b 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 @@ -208,6 +208,30 @@ output: meets_marriage_allowance_income_conditions: false +- name: In 2015-16 a Scottish taxpayer's limit is the basic rate limit + period: 2015 + input: + # 2015-16 had no Scottish rate, and the 2015-16 s. 55B(2)(b) allows the + # basic rate. 42,200 - 10,600 = 31,600, below the basic rate limit, + # though above the 31,500 the model's Scottish scale carries back from + # 2017-18. + employment_income: 42_200 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: true + +- name: In 2016-17 a Scottish taxpayer's limit is the basic rate limit + period: 2016 + input: + # SI 2015/1810 art. 14(3) adds the Scottish basic rate to s. 55B(2)(b), + # and the 2016-17 s. 11A(1) charges it on non-savings income that would + # otherwise bear the basic rate. 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: diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 9ab2ceb4a9..7902b73277 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -26,11 +26,12 @@ The Marriage Allowance income conditions do not read the band. With the nil rates in force, under the law in force or with its rate limits and nil-rate -amounts changed, for every single person in Scotland or England 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. +amounts changed, 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. 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 @@ -317,9 +318,13 @@ def test_savings_allowance_does_not_read_the_band(): # The Marriage Allowance income conditions against the liability, with the nil # rates in force: people alternate between Scotland and England, from 2016-17, -# the first year of the savings and dividend nil rates. +# the first year of the savings and dividend nil rates. Before 2017-18 the +# model charges a Scottish taxpayer's non-savings income on the Scottish scale +# it carries back from 2017-18, where s. 11A(1) charged it on the rest-of-UK +# limits. The conditions follow the law, so the test leaves those people out. MA_YEARS = list(range(2016, 2027)) MA_COUNTRIES = ["SCOTLAND", "ENGLAND"] +FIRST_SCOTTISH_LIMITS_YEAR = 2017 # The rates s. 55B(2)(b) forbids, 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 @@ -339,9 +344,7 @@ def test_savings_allowance_does_not_read_the_band(): ), "gov.hmrc.income_tax.rates.uk[1].threshold": st.integers(25_000, 50_000), "gov.hmrc.income_tax.rates.dividends[1].threshold": st.integers(25_000, 50_000), - "gov.hmrc.income_tax.rates.savings_starter_rate.allowance": st.integers( - 0, 60_000 - ), + "gov.hmrc.income_tax.rates.savings_starter_rate.allowance": st.integers(0, 60_000), "gov.hmrc.income_tax.allowances.personal_savings_allowance.basic": st.integers( 0, 10_000 ), @@ -407,8 +410,15 @@ def test_marriage_allowance_follows_the_liability_with_nil_rates( 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, - (forbidden < SOME) & (dividends_above_basic < SOME), + meets[checked], + ((forbidden < SOME) & (dividends_above_basic < SOME))[checked], err_msg=f"{drawn} in {period} with {changes}", ) 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 37e214300a..d65aebd3e8 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 @@ -33,16 +33,19 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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. - # The model holds no Scottish scale before 2017-18, and in 2016-17 - # s. 11A(1) charged the Scottish basic rate up to the basic rate limit. + # Scottish limits start in 2017-18: in 2016-17 s. 11A(1) charged the + # Scottish basic rate on income that would otherwise bear the basic rate, + # and 2015-16 had no Scottish rate. The model's Scottish scale carries its + # 2017-18 values back to earlier years, so those years use the basic rate + # limit here. scottish = rates.scotland.rates basic_rate_limit = rates.uk.thresholds[1] + gross_gift - if len(scottish.rates) > 3: - scottish_limit = scottish.thresholds[3] + 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 = basic_rate_limit + scottish_limit = scottish.thresholds[3] + gross_gift non_savings_limit = where( person("pays_scottish_income_tax", period), scottish_limit, From 6d517bb95a88a3648f6438c0520b8e644e03723a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 05:10:09 -0500 Subject: [PATCH 13/33] Give the Scottish property-income cases profit at the limit property_income is profit, and with no receipts given the model applies no property allowance to a profit above 1,000, so the two cases now give 31,092 and 31,093 of profit directly. Co-Authored-By: Claude Opus 5.5 --- .../finance/tax/income_tax/scottish_rates.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 79a99c54e6..7e5ed398f2 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 @@ -657,14 +657,16 @@ person: age: 40 employment_income: 12_570 - property_income: 32_092 + property_income: 31_092 households: household: members: [person] country: SCOTLAND output: - # The 1,000 property allowance leaves 31,092 of property income, and pay - # takes the personal allowance. Property income bears the Scottish rates + # property_income is profit. With no receipts given, the model takes a + # profit above the 1,000 property allowance to reflect the better of + # expenses and the allowance already, and 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 @@ -679,7 +681,7 @@ person: age: 40 employment_income: 12_570 - property_income: 32_093 + property_income: 31_093 households: household: members: [person] From 7e70a75d5db35d84c01801b5c084594c3e25de37 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 05:50:42 -0500 Subject: [PATCH 14/33] Test the removed eligible bands through the reform entry points Scenario's parameter_changes takes numbers only, so a list of bands failed validation before the removal check. The test now passes the saved list through Reform.from_dict and Scenario.from_reform, which check the path first, and checks the alias stays in the parameter tree. Co-Authored-By: Claude Opus 5.5 --- ...iage_allowance_eligible_bands_parameter.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) 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 index 20d308d795..d2cd2bc862 100644 --- a/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py +++ b/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py @@ -6,12 +6,17 @@ """ 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) @@ -23,14 +28,29 @@ def reuse_country_model(): yield -def test_removed_eligible_bands_give_directions(): +@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: - Simulation( - situation={"people": {"person": {"age": {2025: 40}}}}, - scenario=Scenario(parameter_changes={REMOVED: {"2025": ["BASIC"]}}), - ) + 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 From cd974a44ec6e5201a4780221e0bd8fc74b005333 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 06:04:40 -0500 Subject: [PATCH 15/33] Cite the 2016-17 start of s. 11A in the rate test's notes FA 2014 Sch. 38 para. 12 gives s. 11A effect from the year the Treasury appointed under s. 25(5) of the Scotland Act 2012, and SI 2015/2000 art. 3 appoints 2016-17. Co-Authored-By: Claude Opus 5.5 --- .../meets_marriage_allowance_income_conditions.yaml | 8 ++++---- .../meets_marriage_allowance_income_conditions.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) 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 32f9daff2b..7ff02f1210 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 @@ -211,10 +211,10 @@ - name: In 2015-16 a Scottish taxpayer's limit is the basic rate limit period: 2015 input: - # 2015-16 had no Scottish rate, and the 2015-16 s. 55B(2)(b) allows the - # basic rate. 42,200 - 10,600 = 31,600, below the basic rate limit, - # though above the 31,500 the model's Scottish scale carries back from - # 2017-18. + # s. 11A takes effect in 2016-17 (SI 2015/2000 art. 3), and the 2015-16 + # s. 55B(2)(b) allows the basic rate. 42,200 - 10,600 = 31,600, below the + # basic rate limit, though above the 31,500 the model's Scottish scale + # carries back from 2017-18. employment_income: 42_200 region: SCOTLAND output: 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 d65aebd3e8..37d4af4c66 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 @@ -33,11 +33,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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. - # Scottish limits start in 2017-18: in 2016-17 s. 11A(1) charged the - # Scottish basic rate on income that would otherwise bear the basic rate, - # and 2015-16 had no Scottish rate. The model's Scottish scale carries its - # 2017-18 values back to earlier years, so those years use the basic rate - # limit here. + # Scottish limits start in 2017-18. s. 11A takes effect in 2016-17 (FA + # 2014 Sch. 38 para. 12, SI 2015/2000 art. 3), when s. 11A(1) charged the + # Scottish basic rate on income that would otherwise bear the basic rate. + # The model's Scottish scale carries its 2017-18 values back to earlier + # years, so those years use the basic rate limit here. scottish = rates.scotland.rates basic_rate_limit = rates.uk.thresholds[1] + gross_gift if period.start.year < 2017 or len(scottish.rates) < 3: From c27c6bb1ea9c5c8536901cff1ce2ae715563995d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 06:54:28 -0500 Subject: [PATCH 16/33] Test the starting rate for savings in the Marriage Allowance conditions Under a starting rate limit raised to 40,000, the model charges 37,700 of 38,000 of interest at the starting rate for savings and the rest at the savings nil rate, both of which s. 55B(2)(b) allows. Baseline limits never separate the starting rate from the savings nil rate in this test. Co-Authored-By: Claude Opus 5.5 --- ...ts_marriage_allowance_income_conditions.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 7ff02f1210..d733658e7e 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 @@ -107,6 +107,23 @@ savings_income_tax: 0.4 meets_marriage_allowance_income_conditions: false +- name: Under a raised starting rate limit, savings at the starting rate meet the conditions + period: 2026 + input: + # The starting rate limit for savings moves to 40,000. Pay takes the + # personal allowance, so all 38,000 of interest is taxable. The model + # charges the 37,700 of it within the basic rate band at the starting + # rate for savings (s. 12), and the 500 savings allowance for income + # above the basic rate limit (s. 12B(3)) covers the other 300 at the + # savings nil rate (s. 12A). s. 55B(2)(b) allows both rates. + gov.hmrc.income_tax.rates.savings_starter_rate.allowance: 40_000 + employment_income: 12_570 + savings_interest_income: 38_000 + region: NORTH_EAST + output: + savings_income_tax: 0 + meets_marriage_allowance_income_conditions: true + - name: Taxable income at the basic rate limit meets the conditions period: 2026 input: From 0d1e2798a1d7863c602ac46c9f0ad98fd34b4723 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 07:01:19 -0500 Subject: [PATCH 17/33] List the Marriage Allowance rate test among the readers of non-savings income Co-Authored-By: Claude Opus 5.5 --- .../non_savings_income_after_allowances.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 d8485dbc20..65abfc9182 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 @@ -11,8 +11,9 @@ class non_savings_income_after_allowances(Variable): "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, taxable_property_income_after_allowances and, " - "for Scottish taxpayers, tax_band read it." + "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( From bb37aa7d44f8c5f6b2e4c2ab409999c7c7f21980 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:12:08 -0500 Subject: [PATCH 18/33] Name who acts in the pre-2017 and starting-rate notes Comments only: the Treasury adds the Scottish basic rate, a reform raises the starting rate limit, and the model reads its 2017-18 Scottish scale in earlier years. Co-Authored-By: Claude Opus 5.5 --- .../tax/income_tax/scottish_rates.yaml | 8 +++--- ..._marriage_allowance_income_conditions.yaml | 28 +++++++++---------- ...iage_allowance_eligible_bands_parameter.py | 4 +-- .../test_tax_band_scottish_properties.py | 7 +++-- ...ts_marriage_allowance_income_conditions.py | 11 ++++---- 5 files changed, 30 insertions(+), 28 deletions(-) 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 7e5ed398f2..36e2960a49 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 @@ -663,10 +663,10 @@ members: [person] country: SCOTLAND output: - # property_income is profit. With no receipts given, the model takes a - # profit above the 1,000 property allowance to reflect the better of - # expenses and the allowance already, and taxes all 31,092. Pay takes - # the personal allowance. Property income bears the Scottish rates + # 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 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 d733658e7e..aaf6d25972 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 @@ -110,12 +110,12 @@ - name: Under a raised starting rate limit, savings at the starting rate meet the conditions period: 2026 input: - # The starting rate limit for savings moves to 40,000. Pay takes the - # personal allowance, so all 38,000 of interest is taxable. The model - # charges the 37,700 of it within the basic rate band at the starting - # rate for savings (s. 12), and the 500 savings allowance for income - # above the basic rate limit (s. 12B(3)) covers the other 300 at the - # savings nil rate (s. 12A). s. 55B(2)(b) allows both rates. + # A reform raises the starting rate limit for savings to 40,000. Pay + # takes the personal allowance, so the model taxes all 38,000 of + # interest. It charges the 37,700 within the basic rate band at the + # starting rate for savings (s. 12), and the 500 savings allowance for + # income above the basic rate limit (s. 12B(3)) covers the other 300 at + # the savings nil rate (s. 12A). Section 55B(2)(b) allows both rates. gov.hmrc.income_tax.rates.savings_starter_rate.allowance: 40_000 employment_income: 12_570 savings_interest_income: 38_000 @@ -228,10 +228,10 @@ - name: In 2015-16 a Scottish taxpayer's limit is the basic rate limit period: 2015 input: - # s. 11A takes effect in 2016-17 (SI 2015/2000 art. 3), and the 2015-16 - # s. 55B(2)(b) allows the basic rate. 42,200 - 10,600 = 31,600, below the - # basic rate limit, though above the 31,500 the model's Scottish scale - # carries back from 2017-18. + # Section 11A takes effect in 2016-17 (SI 2015/2000 art. 3), and the + # 2015-16 s. 55B(2)(b) allows the basic rate. 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: @@ -240,10 +240,10 @@ - name: In 2016-17 a Scottish taxpayer's limit is the basic rate limit period: 2016 input: - # SI 2015/1810 art. 14(3) adds the Scottish basic rate to s. 55B(2)(b), - # and the 2016-17 s. 11A(1) charges it on non-savings income that would - # otherwise bear the basic rate. 42,700 - 11,000 = 31,700, below the - # basic rate limit. + # The Treasury added the Scottish basic rate to s. 55B(2)(b) (SI + # 2015/1810 art. 14(3)), and the 2016-17 s. 11A(1) charges it on + # non-savings income that would otherwise bear the basic rate. + # 42,700 - 11,000 = 31,700, below the basic rate limit. employment_income: 42_700 region: SCOTLAND output: 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 index d2cd2bc862..661341fbcf 100644 --- a/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py +++ b/policyengine_uk/tests/test_removed_marriage_allowance_eligible_bands_parameter.py @@ -1,7 +1,7 @@ """Saved policies naming the Marriage Allowance eligible bands get directions. -The income conditions apply the rate conditions of ITA 2007 s. 55B(2)(b) and -(ba) directly, so nothing reads +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). """ diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 7902b73277..101e494110 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -319,9 +319,10 @@ def test_savings_allowance_does_not_read_the_band(): # The Marriage Allowance income conditions against the liability, with the nil # rates in force: 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 the Scottish scale -# it carries back from 2017-18, where s. 11A(1) charged it on the rest-of-UK -# limits. The conditions follow the law, so the test leaves those people out. +# model charges a Scottish taxpayer's non-savings income on its 2017-18 +# Scottish scale, where s. 11A(1) charged it on the rest-of-UK limits. The +# Marriage Allowance test uses the basic rate limit there, as the law did, so +# this test leaves those people out. MA_YEARS = list(range(2016, 2027)) MA_COUNTRIES = ["SCOTLAND", "ENGLAND"] FIRST_SCOTTISH_LIMITS_YEAR = 2017 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 37d4af4c66..fc7bbd2f8d 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 @@ -33,11 +33,12 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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. - # Scottish limits start in 2017-18. s. 11A takes effect in 2016-17 (FA - # 2014 Sch. 38 para. 12, SI 2015/2000 art. 3), when s. 11A(1) charged the - # Scottish basic rate on income that would otherwise bear the basic rate. - # The model's Scottish scale carries its 2017-18 values back to earlier - # years, so those years use the basic rate limit here. + # Before 2017-18 a Scottish taxpayer's limit is the basic rate limit. + # Section 11A takes effect in 2016-17 (FA 2014 Sch. 38 para. 12, SI + # 2015/2000 art. 3), when s. 11A(1) charged the Scottish basic rate on + # income that would otherwise bear the basic rate. 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: From 3af8cb7854a7b790cb003156f2d42f889a068143 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:14:57 -0500 Subject: [PATCH 19/33] Say the model picks a Scottish taxpayer's band in its documentation Co-Authored-By: Claude Opus 5.5 --- .../gov/hmrc/income_tax/bracketized_liability/tax_band.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 457cd7f9c7..a2ed900f38 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 @@ -99,9 +99,9 @@ class tax_band(Variable): entity = Person label = "Tax band of the individual" documentation = ( - "The highest band in which the person has taxable income. A " - "Scottish taxpayer's band is the higher of the Scottish band of " - "their income other than savings and dividends and the rest-of-UK " + "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 " From e380c79a779adceeb63202314373781bc897a3b2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 11:41:34 -0500 Subject: [PATCH 20/33] Start the dividend upper rate at the basic rate limit in the rate test Section 13(2) charges the dividend upper rate on dividends that would otherwise bear the higher rate, which s. 10(3) starts at the basic rate limit. The model's dividend scale holds 37,500 in 2015-16, against a 32,000 basic rate limit, so the test read it there and admitted dividends the statute charges at the upper rate. The property ties the dividend scale to a reformed basic rate limit instead of drawing it on its own. Co-Authored-By: Claude Opus 5.5 --- ..._marriage_allowance_income_conditions.yaml | 28 +++++++++++++++++++ .../test_tax_band_scottish_properties.py | 11 ++++++-- ...ts_marriage_allowance_income_conditions.py | 9 +++--- 3 files changed, 42 insertions(+), 6 deletions(-) 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 aaf6d25972..b3ba7d9ce1 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 @@ -287,3 +287,31 @@ 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. Section 13(2) charges the dividend upper rate on + # dividends that would otherwise bear the higher rate, which starts at + # the basic rate limit (s. 10(3)). The 2015-16 s. 55B(2)(b) permits the + # dividend ordinary rate only. 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 s. 13(2) charges at the dividend upper rate. + 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 diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 101e494110..45bd0b4a28 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -337,14 +337,15 @@ def test_savings_allowance_does_not_read_the_band(): ) 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" # 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 ), - "gov.hmrc.income_tax.rates.uk[1].threshold": st.integers(25_000, 50_000), - "gov.hmrc.income_tax.rates.dividends[1].threshold": st.integers(25_000, 50_000), + BASIC_RATE_LIMIT: st.integers(25_000, 50_000), "gov.hmrc.income_tax.rates.savings_starter_rate.allowance": st.integers(0, 60_000), "gov.hmrc.income_tax.allowances.personal_savings_allowance.basic": st.integers( 0, 10_000 @@ -404,6 +405,12 @@ def test_marriage_allowance_follows_the_liability_with_nil_rates( 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)) + ) + # Section 13(2) starts the dividend upper rate at the basic rate limit + # (s. 10(3)), so a reformed limit moves the model's dividend scale too. + changes[DIVIDEND_UPPER_START] = basic_rate_limit with law(sim, period, changes): meets = np.asarray( sim.calculate("meets_marriage_allowance_income_conditions", period) 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 fc7bbd2f8d..14f919324e 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 @@ -77,15 +77,16 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): non_savings + savings > basic_rate_limit ) # Dividends are the top slice, above all non-savings income and savings - # (s. 16), counted before the dividend allowance. The dividend upper rate - # starts at the basic rate limit (s. 13(2)), extended by the gross gift; - # dividend_income_tax reads that limit from the dividend scale. + # (s. 16), counted before the dividend allowance. Section 13(2) charges + # the dividend upper rate on dividends that would otherwise bear the + # higher rate, which starts at the basic rate limit (s. 10(3)), extended + # by the gross gift. dividends = max_( 0, person("taxable_dividend_income", period) - person("received_allowances_dividend_income", period), ) - dividend_upper_limit = rates.dividends.thresholds[1] + gross_gift + dividend_upper_limit = basic_rate_limit dividends_above_basic = (dividends > 0) & ( non_savings + savings + dividends > dividend_upper_limit ) From 743b277e39a613d0be7004388d79a1a99599a6a2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 11:41:39 -0500 Subject: [PATCH 21/33] Count no savings nil rate in the rate test before 2016-17 FA 2016 s. 4 inserted the savings nil rate (ss. 12A and 12B) and added it to s. 55B(2)(b), with effect for 2016-17 and later years (s. 4(17)). The model's Personal Savings Allowance amounts start in 2005, so the test counted a nil rate in 2015-16 that the law didn't have. Co-Authored-By: Claude Opus 5.5 --- ..._marriage_allowance_income_conditions.yaml | 25 +++++++++++++++++++ ...ts_marriage_allowance_income_conditions.py | 7 +++++- 2 files changed, 31 insertions(+), 1 deletion(-) 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 b3ba7d9ce1..ea9f8cf2e1 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 @@ -315,3 +315,28 @@ 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. FA 2016 s. 4(17) gives the savings + # nil rate effect from 2016-17, so the 2015-16 s. 55B(2)(b) permits no + # savings nil rate, and the pound bears the higher rate. 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: 2015-16 has + # no 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/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 14f919324e..65142de572 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 @@ -72,7 +72,12 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 0, max_(0, starter.allowance - non_savings * starter.phase_out), ) - nil_rate_savings = starting_rate_savings + person("savings_allowance", period) + # FA 2016 s. 4(17) gives the savings nil rate effect from 2016-17. + if period.start.year < 2016: + savings_allowance = 0 + else: + savings_allowance = person("savings_allowance", period) + nil_rate_savings = starting_rate_savings + savings_allowance savings_above_basic = (savings > nil_rate_savings) & ( non_savings + savings > basic_rate_limit ) From d96eb3d7ae838b5607a99d64461f9b7ab1afb76e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 11:42:48 -0500 Subject: [PATCH 22/33] Place the savings nil rate above the starting rate limit in the rate test Section 12(1) charges the starting rate for savings only on savings that would otherwise bear a basic rate, so it never reaches past the basic rate limit. Section 12A(1) to (4) charges the savings nil rate on the lowest savings above the starting rate limit, up to the savings allowance. The test now fails a person when any savings above the basic rate limit lie outside that slice, instead of starting the slice where the starting rate ends. Under a starting rate limit of 40,000, 12,570 of pay and 38,000 of interest leave 300 above the basic rate limit at the savings higher rate, so that case now fails the conditions. A new case under a 37,500 limit meets them: s. 12 charges 37,500 at the starting rate and s. 12A the 500 above it at the savings nil rate. The property keeps a reformed starting rate limit at or below the basic rate limit, where the model's savings liability places the nil rate as s. 12A does. Co-Authored-By: Claude Opus 5.5 --- ..._marriage_allowance_income_conditions.yaml | 34 +++++++++++++---- .../test_tax_band_scottish_properties.py | 10 ++++- ...ts_marriage_allowance_income_conditions.py | 37 +++++++++---------- 3 files changed, 53 insertions(+), 28 deletions(-) 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 ea9f8cf2e1..23c84aeda7 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 @@ -107,23 +107,41 @@ savings_income_tax: 0.4 meets_marriage_allowance_income_conditions: false -- name: Under a raised starting rate limit, savings at the starting rate meet the conditions +- 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 40,000. Pay - # takes the personal allowance, so the model taxes all 38,000 of - # interest. It charges the 37,700 within the basic rate band at the - # starting rate for savings (s. 12), and the 500 savings allowance for - # income above the basic rate limit (s. 12B(3)) covers the other 300 at - # the savings nil rate (s. 12A). Section 55B(2)(b) allows both rates. - gov.hmrc.income_tax.rates.savings_starter_rate.allowance: 40_000 + # A reform raises the starting rate limit for savings to 37,500. Pay + # uses the personal allowance. Section 12 charges 37,500 of interest at + # the starting rate, and s. 12A charges the 500 above that limit at the + # savings nil rate. Section 55B(2)(b) permits both. + 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: Savings below a raised starting rate limit above 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. Section 12(1)(b) charges the starting + # rate only on income that would otherwise bear the basic rate, so it + # covers 37,700 of the 38,000 of interest. Section 12A applies only to + # savings above the 40,000 limit, and the interest stops below it, so + # the other 300 bears the savings higher rate, which s. 55B(2)(b) + # forbids. 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: diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 45bd0b4a28..6158279e59 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -339,6 +339,7 @@ def test_savings_allowance_does_not_read_the_band(): 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 = { @@ -346,7 +347,7 @@ def test_savings_allowance_does_not_read_the_band(): 20_000, 50_000 ), BASIC_RATE_LIMIT: st.integers(25_000, 50_000), - "gov.hmrc.income_tax.rates.savings_starter_rate.allowance": st.integers(0, 60_000), + STARTING_RATE_LIMIT: st.integers(0, 60_000), "gov.hmrc.income_tax.allowances.personal_savings_allowance.basic": st.integers( 0, 10_000 ), @@ -411,6 +412,13 @@ def test_marriage_allowance_follows_the_liability_with_nil_rates( # Section 13(2) starts the dividend upper rate at the basic rate limit # (s. 10(3)), 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 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) 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 65142de572..8d965f5f12 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 @@ -53,34 +53,33 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): basic_rate_limit, ) # Savings sit above non-savings income (s. 16) and take the rest-of-UK - # bands (s. 11D(6)). As in higher_rate_savings_income, the model charges - # the first of them at the starting rate for savings, which it keeps - # within the basic rate band, and then at the savings nil rate. Savings - # above those bear a positive rate, and the person fails the test once - # they pass the basic rate limit. + # limits (s. 11D(6)). Up to the basic rate limit they bear a basic rate + # or the starting rate for savings, both permitted: s. 12(1) charges the + # starting rate only on savings that would otherwise bear a basic rate + # (s. 12(1)(b) from 2017-18). Above the limit s. 55B(2)(b) permits only + # the savings nil rate. Section 12A(1) to (4) charges it on the lowest + # savings above the starting rate limit for savings, up to the savings + # allowance; dividends sit above savings (s. 16) and leave this slice + # where it is. 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), ) - starter = rates.savings_starter_rate - savings_in_basic_band = clip( - non_savings + savings, rates.uk.thresholds[0], basic_rate_limit - ) - clip(non_savings, rates.uk.thresholds[0], basic_rate_limit) - starting_rate_savings = clip( - min_(savings, savings_in_basic_band), - 0, - max_(0, starter.allowance - non_savings * starter.phase_out), - ) # FA 2016 s. 4(17) gives the savings nil rate effect from 2016-17. if period.start.year < 2016: savings_allowance = 0 else: savings_allowance = person("savings_allowance", period) - nil_rate_savings = starting_rate_savings + savings_allowance - savings_above_basic = (savings > nil_rate_savings) & ( - non_savings + savings > basic_rate_limit + 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 are the top slice, above all non-savings income and savings # (s. 16), counted before the dividend allowance. Section 13(2) charges # the dividend upper rate on dividends that would otherwise bear the @@ -93,11 +92,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): ) dividend_upper_limit = basic_rate_limit dividends_above_basic = (dividends > 0) & ( - non_savings + savings + dividends > dividend_upper_limit + below_dividends + dividends > dividend_upper_limit ) return ( (non_savings <= non_savings_limit) - & ~savings_above_basic + & ~savings_at_forbidden_rates & ~dividends_above_basic ) From 09d216d51850cef0623d12d60ef7ed03e601c8c9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 12:03:00 -0500 Subject: [PATCH 23/33] Describe the rate test and the band in current terms The rate test's notes now say which rates s. 55B(2)(b) permits each year and where the test follows the statute against the model's liability. tax_band, savings_allowance, non_savings_income_after_allowances and the CPS reform's conditions variable describe what each now does, and the Scottish rates cases name the band each reaches. The changelog fragment names the income conditions. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2350.fixed.md | 2 +- .../reforms/cps/marriage_tax_reforms.py | 5 ++- .../tax/income_tax/scottish_rates.yaml | 39 ++++++++++--------- ..._marriage_allowance_income_conditions.yaml | 6 +-- policyengine_uk/tests/test_gift_aid.py | 4 +- .../test_tax_band_scottish_properties.py | 39 ++++++++++--------- ...ts_marriage_allowance_income_conditions.py | 31 ++++++++------- .../allowances/savings_allowance.py | 6 +-- .../non_savings_income_after_allowances.py | 10 ++--- .../bracketized_liability/tax_band.py | 12 +++--- 10 files changed, 81 insertions(+), 73 deletions(-) diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md index 9f9b9e9129..dbcee5972f 100644 --- a/changelog.d/2350.fixed.md +++ b/changelog.d/2350.fixed.md @@ -1 +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. It now tests Marriage Allowance eligibility against the rate conditions of ITA 2007 s. 55B(2)(b) and (ba) directly, so income at a rate limit and savings at the savings nil rate no longer block it. +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. It 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. diff --git a/policyengine_uk/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index 943c237bb8..2ac8da36ac 100644 --- a/policyengine_uk/reforms/cps/marriage_tax_reforms.py +++ b/policyengine_uk/reforms/cps/marriage_tax_reforms.py @@ -37,7 +37,10 @@ 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 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 36e2960a49..6eafafd7d8 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 @@ -132,16 +132,17 @@ # Scottish rates charge only income that is neither savings nor dividends # (ITA 2007 s. 11A(1A); S6M-16531 for 2025-26). Savings and dividends are the # top slice (s. 16) and take the rates and bands they would bear outside -# Scotland (ss. 11D(6) and 13(5)). The band is the higher of the two. In +# Scotland (ss. 11D(6) and 13(5)). The model returns the higher band. In # 2025-26 the personal allowance is £12,570, the Scottish limits on taxable # income are £2,827, £14,921, £31,092, £62,430 and £125,140 (19%, 20%, 21%, # 42%, 45%, 48%), the basic rate limit is £37,700, the dividend allowance is # £500 (8.75% ordinary, 33.75% upper) and the Personal Savings Allowance is # £1,000, or £500 with higher-rate income (s. 12B). Marriage Allowance # applies the rate conditions of s. 55B(2)(b) and the dividend counterfactual -# of s. 55B(2)(ba); savings charged at the savings nil rate leave it open. +# 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 is intermediate +- name: Scottish 2025 pay of 40000 and dividends of 5000 reaches the intermediate band period: 2025 absolute_error_margin: 0.01 input: @@ -163,7 +164,7 @@ earned_income_tax: 5_582.82 dividend_income_tax: 393.75 -- name: Scottish 2025 pay of 40000 and savings of 5000 is intermediate +- name: Scottish 2025 pay of 40000 and savings of 5000 reaches the intermediate band period: 2025 absolute_error_margin: 0.01 input: @@ -186,7 +187,7 @@ earned_income_tax: 5_582.82 savings_income_tax: 800 -- name: England 2025 pay of 40000 and dividends of 5000 is basic +- name: England 2025 pay of 40000 and dividends of 5000 reaches the basic band period: 2025 absolute_error_margin: 0.01 input: @@ -206,7 +207,7 @@ earned_income_tax: 5_486 dividend_income_tax: 393.75 -- name: Scottish 2025 pay of 60000 and dividends of 20000 is higher +- name: Scottish 2025 pay of 60000 and dividends of 20000 reaches the higher band period: 2025 absolute_error_margin: 0.01 input: @@ -230,7 +231,7 @@ earned_income_tax: 13_213.80 dividend_income_tax: 6_581.25 -- name: Scottish 2025 pay of 60000 and savings of 20000 is higher +- name: Scottish 2025 pay of 60000 and savings of 20000 reaches the higher band period: 2025 absolute_error_margin: 0.01 input: @@ -251,7 +252,7 @@ earned_income_tax: 13_213.80 savings_income_tax: 7_800 -- name: England 2025 pay of 60000 and dividends of 20000 is higher +- name: England 2025 pay of 60000 and dividends of 20000 reaches the higher band period: 2025 absolute_error_margin: 0.01 input: @@ -337,7 +338,7 @@ earned_income_tax: 6_351.84 dividend_income_tax: 534.45 -- name: Scottish 2025 dividends one pound above the basic rate limit are higher +- name: Scottish 2025 dividends one pound above the basic rate limit reach the higher band period: 2025 absolute_error_margin: 0.01 input: @@ -383,7 +384,7 @@ earned_income_tax: 6_509.84 dividend_income_tax: 393.75 -- name: Scottish 2025 allowance beyond pay goes to dividends, which are basic +- name: Scottish 2025 allowance beyond pay goes to dividends, which reach the basic band period: 2025 absolute_error_margin: 0.01 input: @@ -405,7 +406,7 @@ earned_income_tax: 0 dividend_income_tax: 1_918.88 -- name: Scottish 2025 allowance beyond pay goes to savings, which are basic +- name: Scottish 2025 allowance beyond pay goes to savings, which reach the basic band period: 2025 absolute_error_margin: 0.01 input: @@ -427,7 +428,7 @@ earned_income_tax: 0 savings_income_tax: 3_286 -- name: Scottish 2025 starter-rate pay without savings or dividends is starter +- name: Scottish 2025 starter-rate pay without savings or dividends reaches the starter band period: 2025 absolute_error_margin: 0.01 input: @@ -461,7 +462,7 @@ tax_band: NONE earned_income_tax: 0 -- name: Scottish 2018-19 pay of 40000 and dividends of 5000 is intermediate +- name: Scottish 2018-19 pay of 40000 and dividends of 5000 reaches the intermediate band period: 2018 absolute_error_margin: 0.01 input: @@ -486,7 +487,7 @@ earned_income_tax: 5_770 dividend_income_tax: 225 -- name: Scottish 2017-18 pay above 43000 is higher +- name: Scottish 2017-18 pay above 43000 reaches the higher band period: 2017 absolute_error_margin: 0.01 input: @@ -511,7 +512,7 @@ # £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 is intermediate +- name: Scottish 2026 pay of 40000 and dividends of 5000 reaches the intermediate band period: 2026 absolute_error_margin: 0.01 input: @@ -532,7 +533,7 @@ meets_marriage_allowance_income_conditions: true earned_income_tax: 5_551.07 -- name: Scottish 2026 pay of 60000 and savings of 20000 is higher +- name: Scottish 2026 pay of 60000 and savings of 20000 reaches the higher band period: 2026 input: people: @@ -568,7 +569,7 @@ tax_band: INTERMEDIATE meets_marriage_allowance_income_conditions: true -- name: Scottish 2026 dividends one pound above the basic rate limit are higher +- name: Scottish 2026 dividends one pound above the basic rate limit reach the higher band period: 2026 input: people: @@ -584,7 +585,7 @@ tax_band: HIGHER meets_marriage_allowance_income_conditions: false -- name: Scottish 2026 allowance beyond pay goes to dividends, which are basic +- name: Scottish 2026 allowance beyond pay goes to dividends, which reach the basic band period: 2026 input: people: @@ -673,7 +674,7 @@ meets_marriage_allowance_income_conditions: true property_income_tax: 6_351.84 -- name: Scottish 2025 property income one pound above the higher rate limit is higher +- name: Scottish 2025 property income one pound above the higher rate limit reaches the higher band period: 2025 absolute_error_margin: 0.01 input: 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 23c84aeda7..ca5a8512e5 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 @@ -21,7 +21,7 @@ - name: Higher-rate earner fails the Marriage Allowance income conditions period: 2021 input: - # 60,000 - 12,570 = 47,430, so 9,730 is at the higher rate. + # 60,000 - 12,570 = 47,430. The model taxes 9,730 at the higher rate. employment_income: 60_000 region: NORTH_EAST output: @@ -30,8 +30,8 @@ - name: Additional-rate earner fails the Marriage Allowance income conditions period: 2021 input: - # No personal allowance above 125,140 of income; 10,000 is above the - # 150,000 higher rate limit. + # 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: diff --git a/policyengine_uk/tests/test_gift_aid.py b/policyengine_uk/tests/test_gift_aid.py index 16df5f01dc..7beb9f00b4 100644 --- a/policyengine_uk/tests/test_gift_aid.py +++ b/policyengine_uk/tests/test_gift_aid.py @@ -374,8 +374,8 @@ def test_zero_gift_preserves_baseline_outputs_bit_for_bit(): # 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 the band is basic, - # not intermediate (#2350). + # 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_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 6158279e59..e9aa8ee7a4 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -9,29 +9,30 @@ 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. -The people are single, so the Marriage Allowance, which the model takes off -earned income rather than giving as a tax reduction (#1947), plays no part. -With the starting rate for savings, the Personal Savings Allowance and the -dividend allowance set to nothing, every pound of taxable income is charged at -a band's rate, and for every single Scottish taxpayer: +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 baseline nil rates restored, the model keeps nil-rate income in its +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 nil -rates in force, under the law in force or with its rate limits and nil-rate -amounts changed, 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 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, as s. 13(2) does, and hold the starting rate limit for +savings at or below the basic rate limit, the range in which the model's +savings liability places the savings nil rate as s. 12A does. 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 @@ -193,12 +194,12 @@ def liability(sim, period): def charged_at(sim, period, paths): - """The income charged at the rates at these paths: the rise in tax when - only those rates rise by RISE, divided by RISE.""" + """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} - # A rate with no value that year (a Scottish rate not yet set) stays so. + # 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 @@ -250,7 +251,7 @@ 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) - # The Marriage Allowance and counts by marginal rate turn on these. + # 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]}" @@ -400,7 +401,7 @@ def test_marriage_allowance_follows_the_liability_with_nil_rates( sim = mixed_sim set_people(sim, drawn, period) parameters = sim.tax_benefit_system.parameters - # A limit with no value that year (a Scottish rate not yet set) stays so. + # Skip Scottish rates that Parliament had not set that year. changes = { path: value for path, value in changes.items() 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 8d965f5f12..ac03c3c674 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 @@ -4,18 +4,21 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): """Whether the person pays no income tax above the basic rates. - ITA 2007 s. 55B(2)(b) and s. 55C(1)(c) allow only the basic rate, the - property, default, savings, Scottish and Welsh basic rates, the Scottish - starter and intermediate rates, the dividend ordinary rate and the nil and - starting rates for savings. So no non-savings income may reach the higher - rate (the Scottish higher rate for a Scottish taxpayer), and no savings the - savings higher rate. Dividends covered by the dividend nil rate still - count: s. 55B(2)(ba) and s. 55C(1)(ca) test the dividend upper rate as if + Apply the rates that s. 55B(2)(b) and s. 55C(1)(c) permit for the year + being calculated. 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. - The bands are the ones the model taxes with: Gift Aid extends the rate - limits by the grossed-up gift (s. 414(2)(b)), as in earned_income_tax, - and does not come off the income. + Gift Aid extends the rate limits by the grossed-up gift (s. 414(2)(b)); + it 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). The test reads income before the Marriage Allowance. Step 4 of s. 23 charges tax at each rate before Step 6 deducts the tax reductions, and @@ -28,14 +31,14 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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). It is non_savings_taxable_income before the + # dividends (s. 16). This reads non-savings taxable income before the # Marriage Allowance. 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 a Scottish taxpayer's limit is the basic rate limit. - # Section 11A takes effect in 2016-17 (FA 2014 Sch. 38 para. 12, SI - # 2015/2000 art. 3), when s. 11A(1) charged the Scottish basic rate on + # Before 2017-18 the test holds a Scottish taxpayer to the basic rate + # limit. Section 11A takes effect in 2016-17 (FA 2014 Sch. 38 para. 12, + # SI 2015/2000 art. 3), when s. 11A(1) charged the Scottish basic rate on # income that would otherwise bear the basic rate. The model reads its # Scottish scale's 2017-18 values in earlier years too, so the test sets # the limit by the year. 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..128ab66674 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,9 +13,9 @@ 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 " + "For Scottish taxpayers, tax_band also uses Scottish limits for " + "non-savings income. This calculation uses the rest-of-UK limits " + "directly. 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))." ) 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 65abfc9182..16ef82ac61 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,11 +6,11 @@ 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. " + "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. Section 16 of ITA 2007 treats savings and " + "dividends as the highest part of income, above this income. Gift Aid " + "raises the rate limits and 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." 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 a2ed900f38..32c242be75 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 @@ -12,8 +12,8 @@ class TaxBand(Enum): def uk_tax_band_above_limits(person, period, parameters): - """The rest-of-UK band before 2018-19: income above a limit is in the - band above it.""" + """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 @@ -35,8 +35,8 @@ def uk_tax_band_above_limits(person, period, parameters): def scottish_tax_band(person, period, parameters): """The highest band in which a Scottish taxpayer has taxable income. - Use the Scottish limits for income other than savings and dividends, - the only income the Scottish rates charge (ITA 2007 s. 11A and the + 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)). @@ -131,8 +131,8 @@ def formula(person, period, parameters): return uk_tax_band_above_limits(person, period, parameters) def formula_2017(person, period, parameters): - # Annual formulas start on 1 January, so this runs from 2017-18, the - # first year of Scottish rates and limits. + # 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), From 7ad623f0c74609e350fef7b041bb7c518af0f4e9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 13:15:38 -0500 Subject: [PATCH 24/33] Name who acts in the new rate-test case notes Comments and one case name only: the 2015-16 cases say which rates s. 55B(2)(b) permits, and the property's Marriage Allowance section names its nil-rate amounts and reforms. Co-Authored-By: Claude Opus 5.5 --- ...s_marriage_allowance_income_conditions.yaml | 18 +++++++++--------- .../tests/test_tax_band_scottish_properties.py | 7 ++++--- 2 files changed, 13 insertions(+), 12 deletions(-) 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 ca5a8512e5..6ce9196d4c 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 @@ -124,7 +124,7 @@ savings_income_tax: 0 meets_marriage_allowance_income_conditions: true -- name: Savings below a raised starting rate limit above the basic rate limit fail the conditions +- 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 @@ -312,9 +312,9 @@ # 42,600 - 10,600 = 32,000 of pay, at the model's basic rate limit, then # 1 of dividends. Section 13(2) charges the dividend upper rate on # dividends that would otherwise bear the higher rate, which starts at - # the basic rate limit (s. 10(3)). The 2015-16 s. 55B(2)(b) permits the - # dividend ordinary rate only. The model's dividend scale starts the - # upper rate at 37,500 that year. + # the basic rate limit (s. 10(3)). Among the dividend rates, the 2015-16 + # s. 55B(2)(b) permits only the dividend ordinary rate. The model's + # dividend scale starts the upper rate at 37,500 that year. employment_income: 42_600 dividend_income: 1 region: NORTH_EAST @@ -338,9 +338,9 @@ period: 2015 input: # 32,000 of pay, then 1 of interest. FA 2016 s. 4(17) gives the savings - # nil rate effect from 2016-17, so the 2015-16 s. 55B(2)(b) permits no - # savings nil rate, and the pound bears the higher rate. The model - # applies its Personal Savings Allowance in 2015-16 too. + # nil rate effect from 2016-17, so in 2015-16 the pound bears the higher + # rate, which s. 55B(2)(b) forbids. The model applies its Personal + # Savings Allowance in 2015-16 too. employment_income: 42_600 savings_interest_income: 1 region: NORTH_EAST @@ -350,8 +350,8 @@ - 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: 2015-16 has - # no savings nil rate. + # 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 diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index e9aa8ee7a4..04e35fcd3e 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -317,9 +317,10 @@ def test_savings_allowance_does_not_read_the_band(): ) -# The Marriage Allowance income conditions against the liability, with the nil -# rates in force: people alternate between Scotland and England, from 2016-17, -# the first year of the savings and dividend nil rates. Before 2017-18 the +# 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 s. 11A(1) charged it on the rest-of-UK limits. The # Marriage Allowance test uses the basic rate limit there, as the law did, so From f0f576a4451adb83eee5f7bcccbf58f7ffdb81ea Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 16:27:19 -0500 Subject: [PATCH 25/33] Give the Gift Aid rise to s. 414(2)(b) in the rate-test notes Documentation only: the rate test's docstring and non_savings_income_after_allowances name s. 414(2)(b) as what raises the rate limits, and the docstring drops a passive phrase. Co-Authored-By: Claude Opus 5.5 --- ...ts_marriage_allowance_income_conditions.py | 23 +++++++++---------- .../non_savings_income_after_allowances.py | 5 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) 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 ac03c3c674..c7b2dfdd9c 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 @@ -4,21 +4,20 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): """Whether the person pays no income tax above the basic rates. - Apply the rates that s. 55B(2)(b) and s. 55C(1)(c) permit for the year - being calculated. 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. + Apply the rates that s. 55B(2)(b) and s. 55C(1)(c) permit in the tax year. + 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. - Gift Aid extends the rate limits by the grossed-up gift (s. 414(2)(b)); - it does not reduce taxable income. Before 2017-18 this test uses the + Section 414(2)(b) extends the rate limits by the grossed-up Gift Aid gift, + which 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). + 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). The test reads income before the Marriage Allowance. Step 4 of s. 23 charges tax at each rate before Step 6 deducts the tax reductions, and 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 16ef82ac61..e60edea382 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 @@ -9,8 +9,9 @@ class non_savings_income_after_allowances(Variable): "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. Section 16 of ITA 2007 treats savings and " - "dividends as the highest part of income, above this income. Gift Aid " - "raises the rate limits and does not reduce this taxable income. " + "dividends as the highest part of income, above this income. Section " + "414(2)(b) raises the rate limits by the grossed-up Gift Aid gift, " + "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." From 0713b7c9cdcf701ab01242260db5af907d571291 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:01:23 +0000 Subject: [PATCH 26/33] Keep the pound-of-savings case at the savings higher rate under any allocation With pay of 50,070 the least-tax allocation of the personal allowance (ITA 2007 s. 25(2)) can move a pound of allowance onto the savings and take the last pound of interest into the savings nil rate. Pay at the basic rate limit leaves no allocation that lowers the tax, so the case gives 40p of savings tax and fails the conditions either way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GDsfRzbSb9XEm12wG --- .../meets_marriage_allowance_income_conditions.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 6ce9196d4c..289da95f8f 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 @@ -98,8 +98,11 @@ - name: One pound of savings at the savings higher rate fails the conditions period: 2026 input: - # As above with 501 of interest: 1 is at the savings higher rate, 40p. - employment_income: 50_070 + # 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 From c74ca6424849beb39525a83230785ce0fab37ed3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:01:29 +0000 Subject: [PATCH 27/33] Name Parliament, the Scottish Parliament and the Treasury as the actors Rewrite the rate-test, tax_band and test notes so that the body that makes each rule acts, with the section in brackets, and keep statutory quotations as they are. Rename the two pre-2017-18 Scottish cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GDsfRzbSb9XEm12wG --- .../reforms/cps/marriage_tax_reforms.py | 4 +- .../tax/income_tax/scottish_rates.yaml | 47 +++---- ..._marriage_allowance_income_conditions.yaml | 117 +++++++++--------- .../test_marriage_allowance_cps_switch.py | 16 +-- .../test_tax_band_scottish_properties.py | 49 ++++---- ...ts_marriage_allowance_income_conditions.py | 77 ++++++------ .../allowances/savings_allowance.py | 6 +- .../non_savings_income_after_allowances.py | 8 +- .../bracketized_liability/tax_band.py | 24 ++-- 9 files changed, 181 insertions(+), 167 deletions(-) diff --git a/policyengine_uk/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index 2ac8da36ac..2dd2427fa5 100644 --- a/policyengine_uk/reforms/cps/marriage_tax_reforms.py +++ b/policyengine_uk/reforms/cps/marriage_tax_reforms.py @@ -50,8 +50,8 @@ def formula(person, period, parameters): base_eligible = liable_only_at_marriage_allowance_rates( person, period, parameters ) - # Expand to people liable above those rates if the reform's - # expansion conditions are met. + # 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 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 6eafafd7d8..0bfc033c01 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 @@ -128,19 +128,20 @@ earned_income_tax: 50034.35 income_tax: 50034.35 -# tax_band for a Scottish taxpayer with savings or dividends (#2350). The -# Scottish rates charge only income that is neither savings nor dividends -# (ITA 2007 s. 11A(1A); S6M-16531 for 2025-26). Savings and dividends are 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 the personal allowance is £12,570, the Scottish limits on taxable -# income are £2,827, £14,921, £31,092, £62,430 and £125,140 (19%, 20%, 21%, -# 42%, 45%, 48%), the basic rate limit is £37,700, the dividend allowance is -# £500 (8.75% ordinary, 33.75% upper) and the Personal Savings Allowance is -# £1,000, or £500 with higher-rate income (s. 12B). Marriage Allowance -# 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. +# 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); 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 @@ -441,8 +442,8 @@ members: [person] country: SCOTLAND output: - # 1,430 taxable, all at 19% = 271.70. Nothing is savings or dividends, - # so the basic rate limit does not apply. + # 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 @@ -502,8 +503,9 @@ 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. The 2017-18 s. 55B(2)(b) - # allows the Scottish basic rate but not the Scottish higher rate. + # 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 @@ -640,11 +642,12 @@ members: [person] country: SCOTLAND output: - # Income of 125,140 withdraws the whole personal allowance (s. 35), so - # all 125,140 is taxable, up to the 125,140 higher rate limit and not - # above it. With no other income, 5,000 is at the starting rate for - # savings (s. 12) and the higher-rate allowance covers 500 (s. 12B); the - # rest of the 37,700 is at 20% and the 87,440 above it at 40%: + # 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 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 289da95f8f..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,13 +1,13 @@ # 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 the personal -# allowance is £12,570 and the basic rate limit £37,700, and the Scottish -# higher rate starts at £31,092 of taxable income (S6M-16531 for 2025-26, -# S6M-20844 for 2026-27). In 2026-27 the dividend allowance is £500 and the -# Personal Savings Allowance £1,000, or £500 with income above the basic rate -# limit (s. 12B). +# (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 @@ -68,10 +68,10 @@ 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 the Personal Savings - # Allowance is 500 (s. 12B) and charges it at the savings nil rate - # (s. 12A), which s. 55B(2)(b) allows. The model bands the person HIGHER - # on that pound alone. + # 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 @@ -113,10 +113,11 @@ - 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. Section 12 charges 37,500 of interest at - # the starting rate, and s. 12A charges the 500 above that limit at the - # savings nil rate. Section 55B(2)(b) permits both. + # 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 @@ -130,14 +131,14 @@ - 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. Section 12(1)(b) charges the starting - # rate only on income that would otherwise bear the basic rate, so it - # covers 37,700 of the 38,000 of interest. Section 12A applies only to - # savings above the 40,000 limit, and the interest stops below it, so - # the other 300 bears the savings higher rate, which s. 55B(2)(b) - # forbids. The model's savings liability charges these 300 at the - # savings nil rate. + # 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 @@ -148,8 +149,8 @@ - name: Taxable income at the basic rate limit meets the conditions period: 2026 input: - # 50,270 - 12,570 = 37,700: s. 10(2) charges income up to the basic rate - # limit at the basic rate. + # 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: @@ -167,8 +168,9 @@ - name: Scottish taxable income at the start of the higher rate meets the conditions period: 2026 input: - # 43,662 - 12,570 = 31,092: S6M-20844 charges the intermediate rate on - # income up to 31,092 and the higher rate only above it. + # 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: @@ -186,7 +188,7 @@ 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 be at the 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 @@ -199,7 +201,7 @@ - name: Dividends that reach the basic rate limit exactly meet the conditions period: 2026 input: - # 37,500 + 200 = 37,700: none would be at the dividend upper rate. + # 37,500 + 200 = 37,700: none would bear the dividend upper rate. employment_income: 50_070 dividend_income: 200 region: NORTH_EAST @@ -246,24 +248,24 @@ output: meets_marriage_allowance_income_conditions: false -- name: In 2015-16 a Scottish taxpayer's limit is the basic rate limit +- name: In 2015-16 the basic rate limit bounds a Scottish taxpayer's income period: 2015 input: - # Section 11A takes effect in 2016-17 (SI 2015/2000 art. 3), and the - # 2015-16 s. 55B(2)(b) allows the basic rate. 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. + # 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 a Scottish taxpayer's limit is the basic rate limit +- 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 the 2016-17 s. 11A(1) charges it on - # non-savings income that would otherwise bear the basic rate. + # 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 @@ -273,9 +275,9 @@ - name: In 2017-18 the Scottish higher rate fails the conditions period: 2017 input: - # The 2017-18 s. 55B(2)(b) allows the Scottish basic rate only. With an - # 11,500 allowance, the Scottish higher rate starts at 31,500 of taxable - # income: 43,001 - 11,500 = 31,501. + # 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: @@ -302,8 +304,9 @@ - name: In 2018-19 the Scottish intermediate rate meets the conditions period: 2018 input: - # SI 2018/459 art. 6(2) adds the Scottish starter and intermediate rates - # to s. 55B(2)(b). 40,000 - 11,850 = 28,150, at the intermediate rate. + # 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: @@ -312,12 +315,12 @@ - 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. Section 13(2) charges the dividend upper rate on - # dividends that would otherwise bear the higher rate, which starts at - # the basic rate limit (s. 10(3)). Among the dividend rates, the 2015-16 - # s. 55B(2)(b) permits only the dividend ordinary rate. The model's - # dividend scale starts the upper rate at 37,500 that year. + # 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 @@ -327,9 +330,9 @@ - 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 s. 13(2) charges at the dividend upper rate. + # 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 @@ -340,10 +343,10 @@ - name: In 2015-16 savings above the basic rate limit fail the conditions period: 2015 input: - # 32,000 of pay, then 1 of interest. FA 2016 s. 4(17) gives the savings - # nil rate effect from 2016-17, so in 2015-16 the pound bears the higher - # rate, which s. 55B(2)(b) forbids. The model applies its Personal - # Savings Allowance in 2015-16 too. + # 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 diff --git a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py index 5c40b29c71..3825f11522 100644 --- a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py +++ b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py @@ -70,10 +70,10 @@ # (marriage_allowance/rounding_increment.yaml). PERSONAL_ALLOWANCE = 12_570 ROUNDING_INCREMENT = 10 -# 2025-26 taxable income above which non-savings income is charged above the -# rates that allow 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). +# 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. @@ -288,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 rates -# allow the allowance (ITA 2007 s. 55B(2)(b)). +# 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"] @@ -383,8 +383,8 @@ def run(simulation, drawn, anyone_claims=True): # Simulation.calculate returns an enum's names. band=np.asarray(simulation.calculate("tax_band", YEAR), dtype=str), income_tax=simulation.calculate("income_tax", YEAR), - # The earners have only employment income, so all their taxable - # income is non-savings income. + # 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), diff --git a/policyengine_uk/tests/test_tax_band_scottish_properties.py b/policyengine_uk/tests/test_tax_band_scottish_properties.py index 04e35fcd3e..e8e7dcae64 100644 --- a/policyengine_uk/tests/test_tax_band_scottish_properties.py +++ b/policyengine_uk/tests/test_tax_band_scottish_properties.py @@ -1,7 +1,7 @@ """Property tests for a Scottish taxpayer's tax_band and the Marriage Allowance income conditions (#2350). -tax_band is the highest band in which a person has taxable income. For a +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 @@ -30,16 +30,17 @@ 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, as s. 13(2) does, and hold the starting rate limit for -savings at or below the basic rate limit, the range in which the model's -savings liability places the savings nil rate as s. 12A does. +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 (charged at the starting -rate for savings, the dividend nil rate or the dividend ordinary rate), not at -the Scottish starter rate. +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 @@ -302,9 +303,9 @@ def test_band_never_rises_except_from_starter_to_basic(sim, drawn, period): def test_savings_allowance_does_not_read_the_band(): - # ITA 2007 s. 12B(8) sets the Personal Savings Allowance on the - # rest-of-UK bands for everyone, so savings_allowance (#2347) never reads - # tax_band: forcing the band changes nothing. + # 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) @@ -320,18 +321,18 @@ def test_savings_allowance_does_not_read_the_band(): # 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 s. 11A(1) charged it on the rest-of-UK limits. The -# Marriage Allowance test uses the basic rate limit there, as the law did, so -# this test leaves those people out. +# 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 s. 55B(2)(b) forbids, 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. +# 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)] @@ -411,12 +412,14 @@ def test_marriage_allowance_follows_the_liability_with_nil_rates( basic_rate_limit = changes.get( BASIC_RATE_LIMIT, parameters.get_child(BASIC_RATE_LIMIT)(str(period)) ) - # Section 13(2) starts the dividend upper rate at the basic rate limit - # (s. 10(3)), so a reformed limit moves the model's dividend scale too. + # 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 s. 12A only while the - # starting rate limit stays at or below the basic rate limit. + # 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 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 c7b2dfdd9c..34e837bbcc 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 @@ -4,25 +4,26 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): """Whether the person pays no income tax above the basic rates. - Apply the rates that s. 55B(2)(b) and s. 55C(1)(c) permit in the tax year. - 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. + 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. - Section 414(2)(b) extends the rate limits by the grossed-up Gift Aid gift, - which 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 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). - The test reads income before the Marriage Allowance. Step 4 of s. 23 - charges tax at each rate before Step 6 deducts the tax reductions, and - s. 55B(1) gives the allowance as a tax reduction. The model still takes - the allowance off earned_taxable_income (#1947), so + The test reads income before the Marriage Allowance. 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 (s. 55B(1)). The + model still takes the allowance off earned_taxable_income (#1947), so non_savings_taxable_income and the savings rate bands built on it would read this test back. """ @@ -36,11 +37,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): # 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. Section 11A takes effect in 2016-17 (FA 2014 Sch. 38 para. 12, - # SI 2015/2000 art. 3), when s. 11A(1) charged the Scottish basic rate on - # income that would otherwise bear the basic rate. The model reads its - # Scottish scale's 2017-18 values in earlier years too, so the test sets - # the limit by the year. + # 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: @@ -55,21 +56,22 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 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: s. 12(1) charges the + # 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)(b) from 2017-18). Above the limit s. 55B(2)(b) permits only - # the savings nil rate. Section 12A(1) to (4) charges it on the lowest + # (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; dividends sit above savings (s. 16) and leave this slice - # where it is. The person fails the test if any savings above the basic - # rate limit lie outside the slice. + # 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), ) - # FA 2016 s. 4(17) gives the savings nil rate effect from 2016-17. + # Parliament gave the savings nil rate effect from 2016-17 (FA 2016 + # s. 4(17)). if period.start.year < 2016: savings_allowance = 0 else: @@ -82,11 +84,11 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): 0, nil_rate_top - max_(nil_rate_start, basic_rate_limit) ) savings_at_forbidden_rates = savings_above_basic - nil_rate_above_basic > 0 - # Dividends are the top slice, above all non-savings income and savings - # (s. 16), counted before the dividend allowance. Section 13(2) charges - # the dividend upper rate on dividends that would otherwise bear the - # higher rate, which starts at the basic rate limit (s. 10(3)), extended - # by the gross gift. + # 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) @@ -106,8 +108,9 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): class meets_marriage_allowance_income_conditions(Variable): label = "Meets Marriage Allowance income conditions" documentation = ( - "Whether this person pays income tax only at rates that ITA 2007 " - "s. 55B(2)(b) and (ba) allow for receiving the Marriage Allowance." + "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 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 128ab66674..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 @@ -15,9 +15,9 @@ class savings_allowance(Variable): "Welsh taxpayer, so the bands are the rest-of-UK ones for everyone. " "For Scottish taxpayers, tax_band also uses Scottish limits for " "non-savings income. This calculation uses the rest-of-UK limits " - "directly. 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))." + "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 e60edea382..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 @@ -8,10 +8,10 @@ class non_savings_income_after_allowances(Variable): documentation = ( "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. Section 16 of ITA 2007 treats savings and " - "dividends as the highest part of income, above this income. Section " - "414(2)(b) raises the rate limits by the grossed-up Gift Aid gift, " - "which does not reduce this taxable income. " + "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." 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 32c242be75..6f2b41cabb 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 @@ -42,11 +42,11 @@ def scottish_tax_band(person, period, parameters): 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: s. 10(2) to (3A) charges income "up to" a limit at the - lower rate, and each resolution charges a rate "on income above" one - limit "and up to" the next. 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). + 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 rate + "on income above" one limit "and up to" the next. 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) @@ -64,11 +64,12 @@ def scottish_tax_band(person, period, parameters): 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. Marriage - # Allowance is a tax reduction (s. 55B(1)), so it does not move the band. + # 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) - # s. 414(2)(b) raises the Scottish basic rate's upper limit and every - # limit above it by the grossed-up gift. + # 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) @@ -77,8 +78,9 @@ def scottish_tax_band(person, period, parameters): scottish_band = labels[limits_exceeded] uk_limits = rates.uk.thresholds taxable_income = person("net_income", period) - person("allowances", period) - # Taxable income above the non-savings income is savings or dividends. - # s. 414(2)(b) raises the basic and higher rate limits by the gift too. + # 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, From e5b16d14bbf9456945986565798902b720d222f3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 11:17:54 -0500 Subject: [PATCH 28/33] Cite the Scottish rate resolutions and s. 11A(5) in two comments The tax_band docstring names each Scottish rate resolution as the source of "on income above" one limit "and up to" the next, and the Scottish rates YAML comment cites ITA 2007 s. 11A(1A), (4) and (5) and s. 13, since s. 11A itself excludes only savings income. Text only. Co-Authored-By: Claude Opus 5.5 --- .../baseline/finance/tax/income_tax/scottish_rates.yaml | 3 ++- .../hmrc/income_tax/bracketized_liability/tax_band.py | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) 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 0bfc033c01..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 @@ -130,7 +130,8 @@ # 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); S6M-16531 for 2025-26). +# 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 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 6f2b41cabb..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 @@ -43,10 +43,11 @@ def scottish_tax_band(person, period, parameters): 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 rate - "on income above" one limit "and up to" the next. 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). + 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) From f20faa47664bf2ca276a0003da1603e6e464a490 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 20:01:19 +0000 Subject: [PATCH 29/33] Give the Marriage Allowance as a Step 6 tax reduction at the basic rate Parliament gives the recipient a tax reduction of the appropriate percentage of the transferable amount (ITA 2007 s. 55B(1) and (3)) at Step 6 (s. 55A(2)), limited by s. 29(2) and (3). The model took the allowance off earned_taxable_income instead, so it saved the recipient tax at their top non-savings rate (21% for a Scottish intermediate-rate taxpayer) and widened the starting rate band for savings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J1pBzUscCNHPteLKz --- changelog.d/2350.fixed.md | 2 +- docs/engineering/gift-aid.md | 5 +- .../allowances/marriage_allowance/max.yaml | 2 +- .../earned_taxable_income_exclusions.yaml | 1 - .../income_tax/step_6_tax_reductions.yaml | 10 + .../income/uc_earnings_deductions.yaml | 1 - .../marriage_allowance_tax_reduction.yaml | 372 ++++++++++++++++++ .../income_tax/property_income_band_base.yaml | 12 +- ...iage_allowance_tax_reduction_properties.py | 194 +++++++++ ...st_property_income_band_base_properties.py | 7 +- ...nsion_credit_self_employment_deductions.py | 11 +- .../allowances/marriage_allowance.py | 6 + ...rriage_allowance_appropriate_percentage.py | 46 +++ .../marriage_allowance_tax_reduction.py | 45 +++ ...ts_marriage_allowance_income_conditions.py | 16 +- .../non_savings_taxable_income.py | 8 +- 16 files changed, 709 insertions(+), 29 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.yaml create mode 100644 policyengine_uk/tests/test_marriage_allowance_tax_reduction_properties.py create mode 100644 policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py create mode 100644 policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py diff --git a/changelog.d/2350.fixed.md b/changelog.d/2350.fixed.md index dbcee5972f..3942a7effa 100644 --- a/changelog.d/2350.fixed.md +++ b/changelog.d/2350.fixed.md @@ -1 +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. It 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. +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. 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/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/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/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..3edd770b1f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.yaml @@ -0,0 +1,372 @@ +# 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] + +- 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] + +- 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/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/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/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 34e837bbcc..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 @@ -20,19 +20,19 @@ def liable_only_at_marriage_allowance_rates(person, period, parameters): s. 4(17)), and places the nil-rate slice above the starting rate limit for savings (s. 12A). - The test reads income before the Marriage Allowance. 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 (s. 55B(1)). The - model still takes the allowance off earned_taxable_income (#1947), so - non_savings_taxable_income and the savings rate bands built on it would - read this test back. + 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). This reads non-savings taxable income before the - # Marriage Allowance. + # 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. 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 = [ From 988bd914dc7026707c805240814725e860fd4901 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 20:12:54 +0000 Subject: [PATCH 30/33] Pin Universal Credit's tax on earnings in the Marriage Allowance reduction cases Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J1pBzUscCNHPteLKz --- .../allowances/marriage_allowance_tax_reduction.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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 index 3edd770b1f..fc82157caa 100644 --- 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 @@ -106,6 +106,9 @@ 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 @@ -197,6 +200,7 @@ # (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 From 89f2f9f34d3a39ea5909e1f8e3b8feed8047392e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 21:41:58 +0000 Subject: [PATCH 31/33] Give the CPS reform's higher-rate recipient the basic rate of the allowance The switch test pinned the reform's values from when the model took the allowance off taxable income, worth 40% to a higher-rate earner. The recipient's reduction is 20% of the amount (ITA 2007 s. 55B(1) and (3)). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6J1pBzUscCNHPteLKz --- .../test_marriage_allowance_cps_switch.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py index 3825f11522..6c305411ee 100644 --- a/policyengine_uk/tests/test_marriage_allowance_cps_switch.py +++ b/policyengine_uk/tests/test_marriage_allowance_cps_switch.py @@ -29,8 +29,8 @@ 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 @@ -168,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]), ), } @@ -199,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( From 2ca7c5ff77ed88c8a00580d0a659f54637049133 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 4 Nov 2026 13:00:47 +0000 Subject: [PATCH 32/33] Update the Scottish Pension Credit cases to the Step 6 Marriage Allowance reduction Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GW4VyFJ2Wtu8apY8j --- .../pension_credit_earnings_disregard.yaml | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) 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..975138b2e0 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 already exceeds 14,921 without the earnings, so the + # earnings do not cross into the intermediate band: all 1,040 bear 21%, + # adding 3,029.22 - 2,810.82 = 218.40 of tax. No Class 1 above State + # Pension age, so net earnings are 1,040 - 218.40 = 821.60, and the 20 + # pound disregard (attendance allowance) of 1,040 is capped there. + # 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 From e511964cb5d3408713bf16910857c0ad00b595b7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 4 Nov 2026 09:02:51 -0500 Subject: [PATCH 33/33] Clarify the Scottish Pension Credit case comment The taxable income exceeds 14,921 even without the earnings, so all 1,040 of them bear 21%. The claimant pays no Class 1 contributions above State Pension age, and the 20 pound disregard covers all 821.60 of net earnings. No expected value changes. Co-Authored-By: Claude Opus 5.5 --- .../pension_credit_earnings_disregard.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 975138b2e0..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 @@ -347,11 +347,11 @@ # = 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 already exceeds 14,921 without the earnings, so the - # earnings do not cross into the intermediate band: all 1,040 bear 21%, - # adding 3,029.22 - 2,810.82 = 218.40 of tax. No Class 1 above State - # Pension age, so net earnings are 1,040 - 218.40 = 821.60, and the 20 - # pound disregard (attendance allowance) of 1,040 is capped there. + # 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