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/2517.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Carer's Allowance earnings read their pension contribution deduction share (one-half, Computation of Earnings Regulations 1996 regs. 10(4)(b) and 13) from a new parameter, `gov.dwp.carers_allowance.pension_contribution_deduction_rate`, instead of a hard-coded 0.5, so the share can be reformed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
description: Carer's Allowance deducts this share of pension contributions when it calculates the net earnings and net profit that the earnings limit tests.
values:
1996-11-25: 0.5
metadata:
period: year
unit: /1
label: Carer's Allowance earnings pension contribution deduction rate
economy: false
reference:
- title: The Social Security Benefit (Computation of Earnings) Regulations 1996, regulation 10(4)(b) (employed earners)
href: https://www.legislation.gov.uk/uksi/1996/2745/regulation/10
- title: The Social Security Benefit (Computation of Earnings) Regulations 1996, regulation 13(1)(b)(ii), (4)(c) and (10)(b) (self-employed earners)
href: https://www.legislation.gov.uk/uksi/1996/2745/regulation/13
- title: The Social Security Benefit (Computation of Earnings) Regulations (Northern Ireland) 1996, regulation 10(4)(b) (employed earners)
href: https://www.legislation.gov.uk/nisr/1996/520/regulation/10
- title: The Social Security Benefit (Computation of Earnings) Regulations (Northern Ireland) 1996, regulation 13(1)(b)(ii), (4)(c) and (10)(b) (self-employed earners)
href: https://www.legislation.gov.uk/nisr/1996/520/regulation/13
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Carer's Allowance net earnings deduct one-half of pension contributions
# (Computation of Earnings Regulations 1996 reg. 10(4)(b)) and of pension
# premiums from trading profit (regs. 13(1)(b)(ii), (4)(c) and (10)(b)). The
# share is gov.dwp.carers_allowance.pension_contribution_deduction_rate. Rates
# other than 0.5 below are model reforms, not current law.
#
# 2026-27: personal allowance, NI primary threshold and Class 4 lower profits
# limit £12,570; Class 4 main rate 6%; basic rate 20%. The earnings limit is
# £204 a week (£10,608 a year) and Carer's Allowance is £86.45 a week. Pay of
# £12,000 is under the allowance and the threshold, so the earnings test
# deducts no tax (it takes PAYE on the pay alone) and no Class 1.

- name: At current law Carer's Allowance earnings deduct half of pension contributions
period: 2026
absolute_error_margin: 0.01
input:
age: 45
employment_income: 12_000
personal_pension_contributions: 3_000
care_hours: 40
country: ENGLAND
output:
# 12,000 - 0.5 x 3,000 = 10,500, or £201.92 a week: under the limit.
pension_contributions: 3_000
ni_class_1_employee: 0
carers_allowance_earnings: 10_500
is_gainfully_employed_for_carers_allowance: false
carers_allowance: 86.45 * 52

- name: A lower share raises earnings by contributions times the change and ends entitlement
period: 2026
absolute_error_margin: 0.01
input:
# Model reform, not current law.
gov.dwp.carers_allowance.pension_contribution_deduction_rate: 0.25
age: 45
employment_income: 12_000
personal_pension_contributions: 3_000
care_hours: 40
country: ENGLAND
output:
# 12,000 - 0.25 x 3,000 = 11,250, or £216.35 a week: over the limit.
# That is 750 more than at half: 3,000 x (0.5 - 0.25).
carers_allowance_earnings: 11_250
is_gainfully_employed_for_carers_allowance: true
carers_allowance_pre_overlap: 0
carers_allowance: 0

- name: At current law a carer with smaller contributions is over the earnings limit
period: 2026
absolute_error_margin: 0.01
input:
age: 45
employment_income: 12_000
personal_pension_contributions: 2_000
care_hours: 40
country: ENGLAND
output:
# 12,000 - 0.5 x 2,000 = 11,000, or £211.54 a week: over the limit.
carers_allowance_earnings: 11_000
is_gainfully_employed_for_carers_allowance: true
carers_allowance: 0

- name: A higher share lowers earnings by contributions times the change and restores entitlement
period: 2026
absolute_error_margin: 0.01
input:
# Model reform, not current law.
gov.dwp.carers_allowance.pension_contribution_deduction_rate: 0.75
age: 45
employment_income: 12_000
personal_pension_contributions: 2_000
care_hours: 40
country: ENGLAND
output:
# 12,000 - 0.75 x 2,000 = 10,500, or £201.92 a week: under the limit.
# That is 500 less than at half: 2,000 x (0.75 - 0.5).
carers_allowance_earnings: 10_500
is_gainfully_employed_for_carers_allowance: false
carers_allowance: 86.45 * 52

- name: A lower share raises trading earnings by premiums times the change
period: 2026
absolute_error_margin: 0.01
input:
# Model reform, not current law.
gov.dwp.carers_allowance.pension_contribution_deduction_rate: 0.4
age: 45
self_employment_income: 15_000
personal_pension_contributions: 7_700
care_hours: 40
country: ENGLAND
output:
# Notional tax 20% x 2,430 = 486 and Class 4 6% x 2,430 = 145.80 (reg.
# 14). At half: 15,000 - 486 - 145.80 - 3,850 = 10,518.20, or £202.27 a
# week, under the limit. At 0.4: 15,000 - 486 - 145.80 - 3,080 =
# 11,288.20, or £217.08 a week, over it: 770 more, 7,700 x (0.5 - 0.4).
carers_allowance_earnings: 11_288.20
is_gainfully_employed_for_carers_allowance: true
carers_allowance: 0

- name: A reform of the Housing Benefit share leaves Carer's Allowance earnings
period: 2026
absolute_error_margin: 0.01
input:
# Model reform, not current law.
gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25
age: 45
employment_income: 12_000
personal_pension_contributions: 2_000
care_hours: 40
country: ENGLAND
output:
# The carer is over the limit, so no Carer's Allowance is paid and the
# £12,000 of pay, less relief on the contributions, is the only income:
# no income tax. Housing Benefit net earnings: 12,000 - 0.25 x 2,000 =
# 11,500. Carer's Allowance keeps its own half: 12,000 - 0.5 x 2,000 =
# 11,000, still £211.54 a week.
income_tax: 0
housing_benefit_net_earnings: 11_500
carers_allowance_earnings: 11_000
is_gainfully_employed_for_carers_allowance: true
carers_allowance: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""Property-based tests for Carer's Allowance's pension contribution share.

The Carer's Allowance earnings limit tests net earnings under the Social
Security Benefit (Computation of Earnings) Regulations 1996, which deduct one
half of pension contributions from an employed earner's net earnings (reg.
10(4)(b)) and one half of pension premiums from a self-employed earner's net
profit (regs. 13(1)(b)(ii), (4)(c) and (10)(b)). Northern Ireland's 1996
Regulations (SR 1996/520) say the same. The model reads the share from
gov.dwp.carers_allowance.pension_contribution_deduction_rate.

Invariants, for any generated population of working-age carers in any UK
country and year, with pay, trading profit (a loss counts as nil) and
employee and personal pension contributions of at most 30% of earnings, so
that no floor binds:

1. Exactness: when the share moves from 0.5 to r, carers_allowance_earnings
falls by exactly the person's contributions x (r - 0.5).
2. Locality: the Housing Benefit share is reformed at the same time to a
different value, so earnings that read Housing Benefit's share would miss
invariant 1.
3. Eligibility follows earnings through the limit: on both sides the person
is gainfully employed exactly when weekly earnings, with a fraction of a
penny disregarded (reg. 5), exceed the limit, and then has no underlying
entitlement. The pence are counted in the precision the model stores
earnings in (float32), as the formula counts them. A lower share can only end entitlement and a higher share can
only restore it.
4. Current law: the share is 0.5 in every year from 2015 (the first year the
model simulates) to 2030.
"""

import numpy as np
from hypothesis import HealthCheck, example, given, settings
from hypothesis import strategies as st

from policyengine_uk import Simulation
from policyengine_uk.system import system

CA_SHARE = "gov.dwp.carers_allowance.pension_contribution_deduction_rate"
HB_SHARE = "gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate"
# Every period the model simulates.
ALL_YEARS = "2000-01-01.2100-12-31"
COUNTRIES = ["ENGLAND", "WALES", "SCOTLAND", "NORTHERN_IRELAND"]
YEARS = [2016, 2020, 2024, 2026, 2030]
# Shares on a grid of twentieths, each exact in binary.
SHARES = [i / 20 for i in range(21) if i != 10]
# The model stores amounts as float32, whose spacing is about 0.008 at the
# largest earnings drawn (up to 120,000), so two stored values can differ from
# the exact difference by more than a penny.
TOLERANCE = 0.05

PROPERTY_SETTINGS = settings(
max_examples=6,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)


@st.composite
def carers(draw):
pay = draw(st.floats(0, 60_000, allow_nan=False))
profit = draw(st.floats(-20_000, 60_000, allow_nan=False))
earnings = pay + max(0.0, profit)
# Net earnings before the pension deduction are more than 60% of
# earnings in every year and country tested (about 70% at the top of
# the range), so contributions of at most 30% leave them positive at
# any share up to one.
employee_share = draw(st.one_of(st.just(0.0), st.floats(0, 0.15)))
personal_share = draw(st.one_of(st.just(0.0), st.floats(0, 0.15)))
return dict(
country=draw(st.sampled_from(COUNTRIES)),
age=draw(st.integers(25, 60)),
employment_income=pay,
self_employment_income=profit,
employee_pension_contributions=round(pay * employee_share, 2),
personal_pension_contributions=round(earnings * personal_share, 2),
)


@st.composite
def reforms(draw):
ca, hb = draw(
st.lists(st.sampled_from(SHARES), min_size=2, max_size=2, unique=True)
)
return dict(ca=ca, hb=hb)


def situation(people, year):
persons, benunits, households = {}, {}, {}
for i, carer in enumerate(people):
name = f"carer_{i}"
persons[name] = {
key: {year: value} for key, value in carer.items() if key != "country"
}
persons[name]["care_hours"] = {year: 40}
benunits[f"b{i}"] = {"members": [name]}
households[f"h{i}"] = {
"members": [name],
"country": {year: carer["country"]},
}
return {"people": persons, "benunits": benunits, "households": households}


VARIABLES = [
"pension_contributions",
"carers_allowance_earnings",
"is_gainfully_employed_for_carers_allowance",
"carers_allowance_pre_overlap",
]


def calculate(people, year, reform=None):
sim = Simulation(situation=situation(people, year), reform=reform)
# Keep each variable's stored dtype: the rounding check below must count
# pence in the precision the formula does.
return {v: np.asarray(sim.calculate(v, year)) for v in VARIABLES}


def over_limit(earnings, limit):
# Reg. 5: a fraction of a penny is disregarded. ``earnings`` is the stored
# array, so this counts the pence as is_gainfully_employed_for_carers_allowance
# does, in float32: promoting to float64 first floors 10,608.52 a year,
# stored as 10,608.51953125, to 204.00 a week instead of 204.01.
return np.floor(earnings / 52 * 100 + 1e-6) / 100 > limit


# A carer one penny a week over the 2026-27 limit (10,608.52 / 52 = 204.01), on
# the float32 boundary described in over_limit.
PENNY_OVER = dict(
country="ENGLAND",
age=45,
employment_income=10_608.52,
self_employment_income=0.0,
employee_pension_contributions=0.0,
personal_pension_contributions=0.0,
)


@PROPERTY_SETTINGS
@given(
people=st.lists(carers(), min_size=1, max_size=24),
shares=reforms(),
year=st.sampled_from(YEARS),
)
@example(people=[PENNY_OVER], shares=dict(ca=0.25, hb=0.75), year=2026)
def test_earnings_fall_by_contributions_times_the_change_in_share(people, shares, year):
base = calculate(people, year)
moved = calculate(
people,
year,
{
CA_SHARE: {ALL_YEARS: shares["ca"]},
HB_SHARE: {ALL_YEARS: shares["hb"]},
},
)
limit = system.parameters(f"{year}-01-01").gov.dwp.carers_allowance.earnings_limit
for i, carer in enumerate(people):
case = (carer, shares, year)
contributions = (
carer["employee_pension_contributions"]
+ carer["personal_pension_contributions"]
)
stored = float(base["pension_contributions"][i])
assert abs(stored - contributions) < TOLERANCE, case
before = float(base["carers_allowance_earnings"][i])
after = float(moved["carers_allowance_earnings"][i])
if contributions > 0:
# Floors are slack: earnings stay positive on both sides.
assert before > 0 and after > 0, case
# Invariants 1 and 2: exactly contributions x the change in the
# Carer's Allowance share, not the Housing Benefit share.
change = before - after
assert abs(change - contributions * (shares["ca"] - 0.5)) < TOLERANCE, case
# Invariant 3: the gainful-employment test follows earnings.
for values in (base, moved):
over = over_limit(values["carers_allowance_earnings"][i], limit)
gainful = bool(values["is_gainfully_employed_for_carers_allowance"][i])
assert gainful == over, case
if gainful:
assert values["carers_allowance_pre_overlap"][i] == 0, case
was = bool(base["is_gainfully_employed_for_carers_allowance"][i])
now = bool(moved["is_gainfully_employed_for_carers_allowance"][i])
if shares["ca"] < 0.5:
assert now or not was, case
else:
assert was or not now, case


def test_the_share_is_one_half_in_every_year():
for year in range(2015, 2031):
for day in ("01-01", "04-30"):
parameters = system.parameters(f"{year}-{day}")
share = parameters.gov.dwp.carers_allowance
assert share.pension_contribution_deduction_rate == 0.5, (year, day)
15 changes: 11 additions & 4 deletions policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ class carers_allowance_earnings(Variable):
documentation = (
"Net earnings from employment and self-employment, as the Carer's "
"Allowance gainful-employment test counts them (Computation of "
"Earnings Regulations 1996), less half of the person's pension "
"contributions. A trading loss counts as nil.\n\n"
"Earnings Regulations 1996), less a share of the person's pension "
"contributions: one half in law (regs. 10(4)(b) and 13(4)(c)), read "
"from gov.dwp.carers_allowance.pension_contribution_deduction_rate. "
"A trading loss counts as nil.\n\n"
"Employment (reg. 10(4)): gross pay less the income tax and primary "
"Class 1 contributions deducted from it. The law deducts the tax "
"actually withheld; the model approximates it as the tax PAYE would "
Expand Down Expand Up @@ -119,7 +121,12 @@ def formula(person, period, parameters):
* class_2_contribution_weeks(period.start.year)
)

half_pension_contributions = 0.5 * person("pension_contributions", period)
# Regs. 10(4)(b) and 13(1)(b)(ii), (4)(c) and (10)(b): one half of
# pension contributions and premiums.
pension_share = parameters(
period
).gov.dwp.carers_allowance.pension_contribution_deduction_rate
pension_deduction = pension_share * person("pension_contributions", period)
return max_(
0,
pay
Expand All @@ -129,5 +136,5 @@ def formula(person, period, parameters):
- trading_tax
- trading_class_4
- trading_class_2
- half_pension_contributions,
- pension_deduction,
)
Loading