diff --git a/changelog.d/2347.fixed.md b/changelog.d/2347.fixed.md new file mode 100644 index 0000000000..06c17f5efc --- /dev/null +++ b/changelog.d/2347.fixed.md @@ -0,0 +1 @@ +Set the Personal Savings Allowance on the rest-of-UK bands for Scottish taxpayers, as ITA 2007 s. 12B(8) requires; give the higher amount at exactly the basic rate limit and the lower amount at exactly the higher rate limit (ITA 2007 s. 10); and use the right bands for periods 2016 and 2017. diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml index 6f2f1f0f16..e8e0e20c0e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml @@ -94,21 +94,27 @@ period: 2020 absolute_error_margin: 0 input: - tax_band: "BASIC" + # 2020 parameters: taxable income £27,500, under the £37,500 basic rate limit. + adjusted_net_income: 40_000 + allowances: 12_500 output: savings_allowance: 1000 - name: Savings allowance at higher rate period: 2020 absolute_error_margin: 0 input: - tax_band: "HIGHER" + # Taxable income £47,500, between £37,500 and £150,000. + adjusted_net_income: 60_000 + allowances: 12_500 output: savings_allowance: 500 - name: No savings allowance at additional rate period: 2020 absolute_error_margin: 0 input: - tax_band: "ADDITIONAL" + # Taxable income £200,000, above the £150,000 higher rate limit. + adjusted_net_income: 200_000 + allowances: 0 output: savings_allowance: 0 - name: Allowances by default diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/savings_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/savings_allowance.yaml new file mode 100644 index 0000000000..3ef8fabad3 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/savings_allowance.yaml @@ -0,0 +1,200 @@ +# Personal Savings Allowance, ITA 2007 s. 12B. A Scottish taxpayer's higher- +# and additional-rate income is what would be charged at those rates if they +# were not a Scottish taxpayer (s. 12B(8)(a)(iv) and (b)(iv)), so the +# allowance follows the rest-of-UK bands: in 2026-27, £37,700 and £125,140 of +# taxable income, with a £12,570 personal allowance tapered above £100,000. +# Savings income is taxed at the UK savings rates. + +- name: Case 1, a Scottish advanced-rate taxpayer below £125,140 gets £500. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 80_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Taxable income 81,000 - 12,570 = 68,430: Scottish advanced rate, but + # UK higher rate. (1,000 - 500) x 40% = 200. + pays_scottish_income_tax: true + tax_band: ADDITIONAL + savings_allowance: 500 + savings_income_tax: 200 + +- name: Case 2, a Scottish higher-rate taxpayer below £37,700 gets £1,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 45_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # Taxable income 46,000 - 12,570 = 33,430: Scottish higher rate, but UK + # basic rate, so the whole £1,000 of interest is covered. + tax_band: HIGHER + savings_allowance: 1_000 + savings_income_tax: 0 + +- name: Case 3, an English taxpayer with the same income is unchanged. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 80_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + tax_band: HIGHER + savings_allowance: 500 + savings_income_tax: 200 + +- name: Case 4, a Scottish taxpayer above £125,140 gets nothing. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 130_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: SCOTLAND + output: + # The personal allowance is tapered to nil, so taxable income is + # 131,000, above £125,140. + savings_allowance: 0 + +- name: Case 5, 2016-17 bands for a higher-rate taxpayer. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 60_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + # Taxable income above the £32,000 basic rate limit but below £150,000. + savings_allowance: 500 + +- name: Case 6, 2016-17 bands for an additional-rate taxpayer. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 200_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + savings_allowance: 0 + +- name: Case 7, the 2016-17 rules also apply to period 2017. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 60_000 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + savings_allowance: 500 + +- name: Case 8, taxable income exactly at the basic rate limit has no higher-rate income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 49_270 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + # 50,270 - 12,570 = 37,700: nothing above the limit (ITA 2007 s. 10(2)-(3)). + savings_allowance: 1_000 + savings_income_tax: 0 + +- name: Case 9, a pound over the basic rate limit is higher-rate income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 49_271 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + savings_allowance: 500 + +- name: Case 10, taxable income exactly at the higher rate limit has no additional-rate income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 124_140 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + # Adjusted net income 125,140 tapers the personal allowance to nil, so + # taxable income is 125,140. + savings_allowance: 500 + +- name: Case 11, a pound over the higher rate limit is additional-rate income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 124_141 + savings_interest_income: 1_000 + households: + household: + members: [person] + country: ENGLAND + output: + savings_allowance: 0 diff --git a/policyengine_uk/tests/test_savings_allowance_properties.py b/policyengine_uk/tests/test_savings_allowance_properties.py new file mode 100644 index 0000000000..1ed0d174f6 --- /dev/null +++ b/policyengine_uk/tests/test_savings_allowance_properties.py @@ -0,0 +1,94 @@ +"""Personal Savings Allowance, ITA 2007 s. 12B. + +A Scottish taxpayer's higher- and additional-rate income is what would be +charged at those rates if they were not a Scottish taxpayer (s. 12B(8)(a)(iv) +and (b)(iv)), so the allowance follows the rest-of-UK bands for everyone. + +Invariants, for single adults in 2026 with pay, savings interest and +dividends drawn freely: + +1. Country: the allowance is the same for a Scottish and an English taxpayer + with the same income. +2. Differential: it equals the statute computed independently here: £1,000 + when taxable income is at or under the £37,700 basic rate limit, £500 at + or under the £125,140 higher rate limit, and nil above (ITA 2007 s. 10(2)- + (3)), with taxable income as total income less the personal allowance, + tapered by £1 for every £2 of income over £100,000. +3. Monotonic: a higher pay never raises the allowance. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PEOPLE = 12 +money = st.integers(min_value=0, max_value=300_000) +small = st.integers(min_value=0, max_value=20_000) +incomes = st.lists(st.tuples(money, small, small), min_size=PEOPLE, max_size=PEOPLE) + + +def _allowances(rows, country): + """One single-adult household per row, all in one simulation.""" + people, benunits, households = {}, {}, {} + for i, (pay, savings, dividends) in enumerate(rows): + name = f"p{i}" + people[name] = { + "age": {YEAR: 40}, + "employment_income": {YEAR: pay}, + "savings_interest_income": {YEAR: savings}, + "dividend_income": {YEAR: dividends}, + } + benunits[f"b{i}"] = {"members": [name]} + households[f"h{i}"] = {"members": [name], "country": {YEAR: country}} + simulation = Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + return np.asarray(simulation.calculate("savings_allowance", YEAR)) + + +BOUNDARY_ROWS = [ + (49_270, 1_000, 0), # taxable 37,700: at the basic rate limit + (49_271, 1_000, 0), # taxable 37,701 + (124_140, 1_000, 0), # taxable 125,140 (allowance tapered to nil) + (124_141, 1_000, 0), # taxable 125,141 +] + [(0, 0, 0)] * (PEOPLE - 4) + + +def _statute(pay, savings, dividends): + income = pay + savings + dividends + allowance = max(0, 12_570 - max(0, income - 100_000) / 2) + taxable = income - allowance + if taxable > 125_140: + return 0 + if taxable > 37_700: + return 500 + return 1_000 + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(rows=incomes) +@example(rows=BOUNDARY_ROWS) +def test_scottish_and_english_taxpayers_get_the_same_allowance(rows): + scottish = _allowances(rows, "SCOTLAND") + english = _allowances(rows, "ENGLAND") + assert np.array_equal(scottish, english) + assert list(scottish) == [_statute(*row) for row in rows] + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(rows=incomes, rise=st.integers(min_value=1, max_value=100_000)) +def test_a_higher_pay_never_raises_the_allowance(rows, rise): + before = _allowances(rows, "SCOTLAND") + after = _allowances([(pay + rise, s, d) for pay, s, d in rows], "SCOTLAND") + assert np.all(after <= before) 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 6c046ff0c0..56f2599a13 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 @@ -5,26 +5,36 @@ class savings_allowance(Variable): value_type = float entity = Person label = "Savings Allowance for the year" + documentation = ( + "The Personal Savings Allowance (ITA 2007 s. 12B): the basic amount " + "with no higher-rate income, the higher amount with higher-rate but " + "no additional-rate income, and the additional amount (nil) with any " + "additional-rate income. For a Scottish or Welsh taxpayer, s. " + "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 adjusted net income less " + "allowances." + ) definition_period = YEAR - reference = "Income Tax Act 2007 s. 12B" + reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/12B" unit = GBP def formula(person, period, parameters): - tax_band = person("tax_band", period) - tax_bands = tax_band.possible_values - amounts = parameters( - period - ).gov.hmrc.income_tax.allowances.personal_savings_allowance + income_tax = parameters(period).gov.hmrc.income_tax + amounts = income_tax.allowances.personal_savings_allowance + thresholds = income_tax.rates.uk.thresholds + taxable_income = person("adjusted_net_income", period) - person( + "allowances", period + ) + # thresholds[1] is the basic rate limit and thresholds[2] the higher + # rate limit. Income up to a limit is charged at the lower rate and + # only income above it at the higher one (ITA 2007 s. 10(2)-(3)), so + # taxable income exactly at a limit has none in the band above. return select( - [ - tax_band == tax_bands.ADDITIONAL, - tax_band == tax_bands.HIGHER, - ( - (tax_band == tax_bands.STARTER) - | (tax_band == tax_bands.BASIC) - | (tax_band == tax_bands.INTERMEDIATE) - ), - tax_band == tax_bands.NONE, - ], - [amounts.additional, amounts.higher, amounts.basic, amounts.basic], + [taxable_income > thresholds[2], taxable_income > thresholds[1]], + [amounts.additional, amounts.higher], + default=amounts.basic, )