From 899183476497e05d4f536ceb5ec6191bf4c83136 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 18:42:11 -0400 Subject: [PATCH 1/3] Read Carer's Allowance's pension contribution share from a parameter carers_allowance_earnings deducted a literal 0.5 of pension contributions. The Computation of Earnings Regulations 1996 set that one-half in regs. 10(4)(b) (employed earners) and 13(1)(b)(ii), (4)(c) and (10)(b) (self-employed earners), unchanged since they came into force on 25 November 1996; Northern Ireland's SR 1996/520 says the same. Add gov.dwp.carers_allowance.pension_contribution_deduction_rate (0.5 from 1996-11-25) citing them and read it in place of the literal, so the share can be reformed. Current-law outputs are unchanged. YAML cases show a reform of the share moving earnings by contributions x the change in share and, through the earnings limit, entitlement; a Hypothesis test states the exactness, locality and eligibility invariants. Fixes #2517 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2517.fixed.md | 1 + .../pension_contribution_deduction_rate.yaml | 17 ++ ...e_pension_contribution_deduction_rate.yaml | 119 ++++++++++++ ..._contribution_deduction_rate_properties.py | 172 ++++++++++++++++++ .../gov/dwp/carers_allowance_earnings.py | 15 +- 5 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2517.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/carers_allowance/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py diff --git a/changelog.d/2517.fixed.md b/changelog.d/2517.fixed.md new file mode 100644 index 0000000000..ce59fe4bc0 --- /dev/null +++ b/changelog.d/2517.fixed.md @@ -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. diff --git a/policyengine_uk/parameters/gov/dwp/carers_allowance/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/dwp/carers_allowance/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..0f6b963c1e --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/carers_allowance/pension_contribution_deduction_rate.yaml @@ -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 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..967954a67b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml @@ -0,0 +1,119 @@ +# 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 no tax or Class 1 is +# due on it. + +- 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: 3_000 + care_hours: 40 + country: ENGLAND + output: + # Housing Benefit net earnings: 12,000 - 0.25 x 3,000 = 11,250. Carer's + # Allowance keeps its own half: 12,000 - 0.5 x 3,000 = 10,500. + housing_benefit_net_earnings: 11_250 + carers_allowance_earnings: 10_500 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 diff --git a/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py b/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py new file mode 100644 index 0000000000..2f5b90c32b --- /dev/null +++ b/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py @@ -0,0 +1,172 @@ +"""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. 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, 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] +TOLERANCE = 0.01 + +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) + return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in VARIABLES} + + +def over_limit(earnings, limit): + # Reg. 5: a fraction of a penny is disregarded. + return np.floor(earnings / 52 * 100 + 1e-6) / 100 > limit + + +@PROPERTY_SETTINGS +@given( + st.lists(carers(), min_size=1, max_size=24), + reforms(), + st.sampled_from(YEARS), +) +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"] + ) + assert abs(base["pension_contributions"][i] - contributions) < TOLERANCE, case + if contributions > 0: + # Floors are slack: earnings stay positive on both sides. + assert base["carers_allowance_earnings"][i] > 0, case + assert moved["carers_allowance_earnings"][i] > 0, case + # Invariants 1 and 2: exactly contributions x the change in the + # Carer's Allowance share, not the Housing Benefit share. + change = ( + base["carers_allowance_earnings"][i] - moved["carers_allowance_earnings"][i] + ) + 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) diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py index b49b6618d0..e3dc980d29 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -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 " @@ -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 @@ -129,5 +136,5 @@ def formula(person, period, parameters): - trading_tax - trading_class_4 - trading_class_2 - - half_pension_contributions, + - pension_deduction, ) From b96ec3fcb40124e0b8510e813e9039b1937b16bb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 21:57:51 -0400 Subject: [PATCH 2/3] Use an untaxed carer for the Housing Benefit locality case The case paid Carer's Allowance, which is taxable, so Housing Benefit net earnings carried a share of the resulting income tax (134.64) that the expected value left out. Use the over-limit carer instead: no allowance is paid and no tax is due, so Housing Benefit net earnings are 12,000 less a quarter of 2,000. Co-Authored-By: Claude Opus 5.5 --- ...e_pension_contribution_deduction_rate.yaml | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml index 967954a67b..723a7a972b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_pension_contribution_deduction_rate.yaml @@ -7,8 +7,8 @@ # 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 no tax or Class 1 is -# due on it. +# £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 @@ -107,13 +107,17 @@ gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25 age: 45 employment_income: 12_000 - personal_pension_contributions: 3_000 + personal_pension_contributions: 2_000 care_hours: 40 country: ENGLAND output: - # Housing Benefit net earnings: 12,000 - 0.25 x 3,000 = 11,250. Carer's - # Allowance keeps its own half: 12,000 - 0.5 x 3,000 = 10,500. - housing_benefit_net_earnings: 11_250 - carers_allowance_earnings: 10_500 - is_gainfully_employed_for_carers_allowance: false - carers_allowance: 86.45 * 52 + # 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 From 99d9703f23569fce3b687990e560a5bf2669691b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:10:45 -0400 Subject: [PATCH 3/3] Count pence in the model's float32 in the eligibility property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The property test promoted carers_allowance_earnings to float64 before repeating the formula's penny rounding. The model stores earnings as float32, so 10,608.52 a year is stored as 10,608.51953125: in float32 the formula counts 20,401 weekly pence (over the £204 limit), but the float64 recomputation floors to 20,400 and expected not gainfully employed. Keep the stored dtype for the rounding check, pin that boundary as an explicit Hypothesis example, and widen the exactness tolerance to 0.05 for float32 spacing at the largest earnings drawn. Found by the GPT-6.1 Sol review. Co-Authored-By: Claude Opus 5.5 --- ..._contribution_deduction_rate_properties.py | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py b/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py index 2f5b90c32b..be9c21dea3 100644 --- a/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py +++ b/policyengine_uk/tests/test_carers_allowance_pension_contribution_deduction_rate_properties.py @@ -21,14 +21,15 @@ 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. A lower share can only end entitlement and a higher share can + 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, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -42,7 +43,10 @@ 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] -TOLERANCE = 0.01 +# 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, @@ -107,20 +111,38 @@ def situation(people, year): def calculate(people, year, reform=None): sim = Simulation(situation=situation(people, year), reform=reform) - return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in VARIABLES} + # 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. + # 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( - st.lists(carers(), min_size=1, max_size=24), - reforms(), - st.sampled_from(YEARS), + 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( @@ -138,16 +160,16 @@ def test_earnings_fall_by_contributions_times_the_change_in_share(people, shares carer["employee_pension_contributions"] + carer["personal_pension_contributions"] ) - assert abs(base["pension_contributions"][i] - contributions) < TOLERANCE, case + 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 base["carers_allowance_earnings"][i] > 0, case - assert moved["carers_allowance_earnings"][i] > 0, case + 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 = ( - base["carers_allowance_earnings"][i] - moved["carers_allowance_earnings"][i] - ) + 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):