Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2347.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
94 changes: 94 additions & 0 deletions policyengine_uk/tests/test_savings_allowance_properties.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Loading