From 70851abb1c6d13e0930497cd457886e5c4f8b3a3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 21:01:31 -0400 Subject: [PATCH 1/2] Count statutory sick, maternity and paternity pay as UC earned income UC Regs 2013 reg. 55(4) treats statutory sick, maternity and paternity pay (and adoption, shared parental, parental bereavement and neonatal care pay, which the model has no variables for) as employed earnings, and reg. 55(5) deducts tax and primary Class 1 NI from "the amount of general earnings or benefits specified in paragraphs (2) to (4)". The model deducted the tax and NI on these payments from earned income but left the payments out, so Universal Credit rose by 15.4p per pound of sick or maternity pay above the thresholds. Adds YAML cases and Hypothesis properties: the award never rises with a claimant's statutory pay (two intended exceptions executed and labelled: reg. 32(2)(b) childcare work condition, ITA 2007 s. 55B(2)(b) Marriage Allowance cliff), sick and maternity pay count exactly as wages, earned income adds up, and a dependant's pay is not the claimants'. Fixes #2337 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2337.fixed.md | 1 + .../income/uc_statutory_pay.yaml | 203 +++++++ .../tests/test_uc_statutory_pay_properties.py | 497 ++++++++++++++++++ .../uc_individual_earned_income_before_mif.py | 26 +- 4 files changed, 722 insertions(+), 5 deletions(-) create mode 100644 changelog.d/2337.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml create mode 100644 policyengine_uk/tests/test_uc_statutory_pay_properties.py diff --git a/changelog.d/2337.fixed.md b/changelog.d/2337.fixed.md new file mode 100644 index 0000000000..17d3f13dbf --- /dev/null +++ b/changelog.d/2337.fixed.md @@ -0,0 +1 @@ +- Counted statutory sick, maternity and paternity pay as Universal Credit earned income (UC Regs 2013 reg. 55(4)). The income tax and National Insurance on them were already deducted from earned income, so Universal Credit rose as the payments rose. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml new file mode 100644 index 0000000000..910c62df7d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml @@ -0,0 +1,203 @@ +# Universal Credit treats statutory sick, maternity and paternity pay as +# employed earnings (UC Regs 2013 reg. 55(4)(a) to (c)). Reg. 55(5) takes +# relievable pension contributions, income tax and primary Class 1 National +# Insurance off "the amount of general earnings or benefits specified in +# paragraphs (2) to (4)", so the tax on the payments comes off with the rest. +# Reg. 55(4)(e) to (h) list statutory adoption, shared parental, parental +# bereavement and neonatal care pay too; the model has no variables for them. +# +# Before this was fixed the payments were left out of earned income while the +# income tax and National Insurance on them were still deducted. Each pound of +# sick or maternity pay above the thresholds then raised the award by +# 0.55 x (20% + 8%) = 15.4p; each pound of paternity pay by 0.55 x 8% = 4.4p. +# +# Hand calculations use the 2026-27 rates: +# - personal allowance 12,570; basic rate 20%; +# - primary Class 1 NI 8% above the primary threshold of 241.73 a week +# (12,569.96 a year); +# - UC standard allowance 424.90 a month (single, 25 or over) or 666.97 +# (couple, one 25 or over); child element 303.94 a month; work allowance +# 427 a month with housing costs; taper 55%. + +- name: Statutory sick pay counts as earnings, and the tax on it comes off with the rest + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + statutory_sick_pay: 2_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + # (20,000 + 2,000 - 12,570) x 20%. + income_tax: [1_886, 0] + uc_income_tax_on_earnings: [1_886, 0] + # (22,000 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: [754.40, 0] + # 22,000 - 1,886 - 754.40. Before this fix: 20,000 - 1,886 - 754.40 + # = 17,359.60. + uc_individual_earned_income: [19_359.60, 0] + # Less the work allowance of 427 x 12 = 5,124. + uc_earned_income: 14_235.60 + # 424.90 x 12 + 303.94 x 12 + 9,600 = 5,098.80 + 3,647.28 + 9,600. + uc_maximum_amount: 18_346.08 + # 18,346.08 - 0.55 x 14,235.60. Before this fix: 11,616.50, which is + # 308 more than the 11,308.50 the same parent gets with no sick pay. + universal_credit: 10_516.50 + +- name: Statutory maternity pay counts as earnings in the same way + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + statutory_maternity_pay: 2_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + income_tax: [1_886, 0] + uc_income_tax_on_earnings: [1_886, 0] + uc_national_insurance_on_earnings: [754.40, 0] + uc_individual_earned_income: [19_359.60, 0] + uc_earned_income: 14_235.60 + universal_credit: 10_516.50 + +- name: The same total paid as wages gives the same award + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 22_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_individual_earned_income: [19_359.60, 0] + uc_earned_income: 14_235.60 + universal_credit: 10_516.50 + +- name: Statutory sick pay alone is earned income + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + statutory_sick_pay: 3_000 + output: + # Below the personal allowance and the primary threshold. + uc_income_tax_on_earnings: 0 + uc_national_insurance_on_earnings: 0 + uc_individual_earned_income: 3_000 + # No work allowance: no child and no limited capability for work. + uc_earned_income: 3_000 + # 424.90 x 12. + uc_maximum_amount: 5_098.80 + # 5,098.80 - 0.55 x 3,000. Before this fix the full 5,098.80. + universal_credit: 3_448.80 + +- name: Joint claimants' statutory paternity and maternity pay are combined earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + father: + age: 30 + employment_income: 9_000 + statutory_paternity_pay: 400 + mother: + age: 30 + statutory_maternity_pay: 6_000 + child: + age: 1 + benunits: + benunit: + members: [father, mother, child] + households: + household: + members: [father, mother, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + # Each is below the personal allowance and the primary threshold, so + # nothing is deducted: 9,000 + 400 and 6,000. + uc_income_tax_on_earnings: [0, 0, 0] + uc_national_insurance_on_earnings: [0, 0, 0] + uc_individual_earned_income: [9_400, 6_000, 0] + # 15,400 less the work allowance of 5,124. + uc_earned_income: 10_276 + # 666.97 x 12 + 303.94 x 12 + 9,600 = 8,003.64 + 3,647.28 + 9,600. + uc_maximum_amount: 21_250.92 + # 21,250.92 - 0.55 x 10,276. Before this fix only the 9,000 of wages + # counted: 21,250.92 - 0.55 x 3,876 = 19,119.12. + universal_credit: 15_599.12 + +- name: National Insurance on statutory paternity pay comes off the earnings that include it + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + employment_income: 12_000 + statutory_paternity_pay: 400 + output: + # (12,400 - 12,569.96) is negative, so no National Insurance, and wages + # of 12,000 are below the personal allowance. + uc_national_insurance_on_earnings: 0 + uc_income_tax_on_earnings: 0 + uc_individual_earned_income: 12_400 + # 5,098.80 - 0.55 x 12,400 is negative. + universal_credit: 0 + +- name: A qualifying young person's statutory sick pay is not the claimant's earned income + # Reg. 22(1)(b) deducts "the claimant's earned income (or, in the case of + # joint claimants, their combined earned income)". + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + statutory_sick_pay: 2_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + output: + uc_individual_earned_income_before_mif: [0, 2_000] + uc_earned_income: 0 + # 424.90 x 12 + 351.88 x 12, the higher child element for a first child + # born before 6 April 2017. The sick pay takes nothing off it. + universal_credit: 9_321.36 diff --git a/policyengine_uk/tests/test_uc_statutory_pay_properties.py b/policyengine_uk/tests/test_uc_statutory_pay_properties.py new file mode 100644 index 0000000000..f1f84036c1 --- /dev/null +++ b/policyengine_uk/tests/test_uc_statutory_pay_properties.py @@ -0,0 +1,497 @@ +"""Property-based tests that Universal Credit counts statutory pay as earnings. + +Universal Credit Regulations 2013 reg. 55(4): statutory sick pay, statutory +maternity pay, statutory paternity pay, statutory adoption pay, statutory +shared parental pay, statutory parental bereavement pay and statutory neonatal +care pay "are to be treated as employed earnings". Reg. 55(5) takes relievable +pension contributions, income tax and primary Class 1 contributions off "the +amount of general earnings or benefits specified in paragraphs (2) to (4)". +The model has variables for the first three payments. It charges National +Insurance on all three (ni_class_1_income) and income tax on sick and maternity +pay (employment_benefits in taxable_employment_income), as it does on wages. + +Before this was fixed the payments were left out of earned income while the +tax and National Insurance on them were still deducted, so the award rose by +the taper rate times that tax as a payment rose. + +Invariants, for any generated population of single claimants and couples, with +or without children, in which one member receives one of the three payments: + +1. Monotonic: raising a claimant's payment, with everything else fixed, never + raises the award. Two rules of law make exceptions, and neither is a fault: + + a. Reg. 32(2)(b) treats a claimant receiving statutory sick, maternity or + paternity pay as in paid work for the childcare costs element. So the + first pound of a payment can open that element for a claimant with no + other paid work. The award then rises by no more than the element does, + and only as the payment starts from nil + (test_first_pound_of_statutory_pay_can_open_the_childcare_element). + b. Income Tax Act 2007 s. 55B(2)(b) withdraws the Marriage Allowance tax + reduction in full once the recipient is liable at a rate above the + basic rate. A pound of pay that crosses that line raises the tax on the + earnings by more than a pound, earned income falls and the award rises. + Wages do the same + (test_marriage_allowance_cliff_is_the_same_as_for_wages), so the + monotonic property is stated for families that do not claim Marriage + Allowance. + +2. Same as wages: statutory sick or maternity pay of s on top of wages of e + gives the same income tax, National Insurance, earned income and award + before the benefit cap as wages of e + s. This holds for Marriage Allowance + claimants too. Two differences in the model's income tax are left out + (counts_as_wages): it charges no income tax on statutory paternity pay, and + it caps pension contribution relief at employment and self-employment + income without statutory pay. The award after the benefit cap is not + compared, because the cap's earnings exception + (is_benefit_cap_exempt_earnings) reads employment income without statutory + pay. + +3. Accounting: a person's earned income before the minimum income floor is + their wages, statutory pay and trading profit (a loss counts as nil) less + their relievable pension contributions and the income tax and National + Insurance on their earnings, floored at nil. + +4. Bounds: a pound more of a payment adds between nothing and a pound to the + recipient's earned income, for families that do not claim Marriage + Allowance. + +5. A dependant's payment changes neither the claimants' earned income nor the + award (reg. 22(1)(b): "the claimant's earned income (or, in the case of + joint claimants, their combined earned income)"). + +Every member's is_claimant_or_partner is set from its generated role, as the +FRS supplies it, and every attribute is set for every member: setting an +input for anyone makes it an input for everyone. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Reg. 55(4) payments the model has variables for. +PAYMENTS = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", +] +# The model charges income tax on these, as on wages (employment_benefits). +TAXED_AS_WAGES = ["statutory_sick_pay", "statutory_maternity_pay"] +# Wages and payments on and around the 2026-27 thresholds: the personal +# allowance and primary threshold (12,570), the basic rate limit (50,270) and +# the personal allowance taper (100,000 to 125,140). +WAGES = [0, 0, 6_000, 12_570, 16_000, 24_000, 50_000, 50_270, 62_000, 100_000, 126_000] +PAY = [0, 0, 1, 300, 2_000, 6_000, 12_570, 20_000, 40_000] +# Float32 awards of tens of thousands of pounds are exact to about a penny. +TOLERANCE = 0.05 + +# The three versions of each family that a simulation holds. +LOW, HIGH, AS_WAGES = range(3) +VERSIONS = [LOW, HIGH, AS_WAGES] + + +@st.composite +def adults(draw, marriage_allowance): + return dict( + age=draw(st.sampled_from([22, 30, 45, 62, 68])), + wages=float(draw(st.sampled_from(WAGES))), + profit=float(draw(st.sampled_from([0, 0, 0, 9_000, 30_000, -4_000]))), + startup=draw(st.booleans()), + pension_contributions=float(draw(st.sampled_from([0, 0, 2_000]))), + pension_income=float(draw(st.sampled_from([0, 0, 6_000]))), + dividends=float(draw(st.sampled_from([0, 0, 4_000]))), + lcwra=draw(st.booleans()), + claims_marriage_allowance=marriage_allowance and draw(st.booleans()), + ) + + +@st.composite +def families(draw, marriage_allowance=False, dependant_recipient=False): + members = draw(st.lists(adults(marriage_allowance), min_size=1, max_size=2)) + child_ages = draw(st.lists(st.sampled_from([1, 6, 13]), max_size=3)) + if dependant_recipient: + # A qualifying young person: 17 and in non-advanced education. + child_ages = [17] + child_ages[:2] + recipient = len(members) + else: + recipient = draw(st.integers(0, len(members) - 1)) + amounts = st.one_of(st.sampled_from(PAY), st.integers(0, 40_000)) + low, high = sorted([draw(amounts), draw(amounts)]) + return dict( + adults=members, + child_ages=child_ages, + childcare=float(draw(st.sampled_from([0, 0, 6_000]))), + region=draw( + st.sampled_from( + ["NORTH_EAST", "LONDON", "SCOTLAND", "WALES", "NORTHERN_IRELAND"] + ) + ), + tenure=draw(st.sampled_from(["RENT_FROM_COUNCIL", "RENT_PRIVATELY"])), + rent=float(draw(st.sampled_from([0, 7_200, 18_000, 36_000]))), + recipient=recipient, + payment=draw(st.sampled_from(PAYMENTS)), + low=float(low), + high=float(high), + ) + + +def populations(**kwargs): + # Several families share one simulation, which costs little more than + # one family does. + return st.lists(families(**kwargs), min_size=4, max_size=10) + + +def member_inputs(family, index, version): + """Inputs for one member of one version of a family. + + LOW and HIGH pay the recipient the lower and higher amount of the + payment. AS_WAGES pays the higher amount as wages instead. + """ + is_adult = index < len(family["adults"]) + is_recipient = index == family["recipient"] + paid = {LOW: family["low"], HIGH: family["high"], AS_WAGES: 0.0}[version] + extra_wages = family["high"] if is_recipient and version == AS_WAGES else 0.0 + if is_adult: + adult = family["adults"][index] + inputs = { + "age": adult["age"], + "is_claimant_or_partner": True, + "employment_income": adult["wages"] + extra_wages, + "self_employment_income": adult["profit"], + "uc_is_in_startup_period": adult["startup"], + "employee_pension_contributions": adult["pension_contributions"], + "private_pension_income": adult["pension_income"], + "dividend_income": adult["dividends"], + "uc_limited_capability_for_WRA": adult["lcwra"], + "would_claim_marriage_allowance": adult["claims_marriage_allowance"], + "childcare_expenses": 0.0, + "current_education": "NOT_IN_EDUCATION", + } + else: + age = family["child_ages"][index - len(family["adults"])] + inputs = { + "age": age, + "is_claimant_or_partner": False, + "employment_income": extra_wages, + "self_employment_income": 0.0, + "uc_is_in_startup_period": False, + "employee_pension_contributions": 0.0, + "private_pension_income": 0.0, + "dividend_income": 0.0, + "uc_limited_capability_for_WRA": False, + "would_claim_marriage_allowance": False, + "childcare_expenses": family["childcare"] if age < 16 else 0.0, + "current_education": "UPPER_SECONDARY" if age >= 16 else "PRIMARY", + } + for payment in PAYMENTS: + inputs[payment] = paid if is_recipient and payment == family["payment"] else 0.0 + return {name: {YEAR: value} for name, value in inputs.items()} + + +class Results: + """One simulation holding every family once per version.""" + + def __init__(self, population): + people, benunits, households = {}, {}, {} + self.first_member = {} + for i, family in enumerate(population): + size = len(family["adults"]) + len(family["child_ages"]) + for version in VERSIONS: + self.first_member[i, version] = len(people) + names = [f"person_{i}_{version}_{j}" for j in range(size)] + for j, name in enumerate(names): + people[name] = member_inputs(family, j, version) + benunits[f"benunit_{i}_{version}"] = {"members": names} + households[f"household_{i}_{version}"] = { + "members": names, + "region": {YEAR: family["region"]}, + "tenure_type": {YEAR: family["tenure"]}, + "rent": {YEAR: family["rent"]}, + } + self.population = population + self.simulation = Simulation( + situation=dict(people=people, benunits=benunits, households=households) + ) + + def families(self, variable, version): + """A benefit unit variable for one version of every family.""" + values = np.array(self.simulation.calculate(variable, YEAR), dtype=float) + return values.reshape(len(self.population), len(VERSIONS))[:, version] + + def person(self, variable, i, version, member): + values = np.array(self.simulation.calculate(variable, YEAR), dtype=float) + return values[self.first_member[i, version] + member] + + def recipient(self, variable, i, version): + return self.person(variable, i, version, self.population[i]["recipient"]) + + +@PROPERTY_SETTINGS +@given(population=populations()) +def test_award_never_rises_with_a_claimants_statutory_pay(population): + results = Results(population) + award = {v: results.families("universal_credit", v) for v in (LOW, HIGH)} + childcare = {v: results.families("uc_childcare_element", v) for v in (LOW, HIGH)} + earned = "uc_individual_earned_income_before_mif" + for i, family in enumerate(population): + if family["low"] > 0: + # The payment is already in payment, so reg. 32(2)(b) changes + # nothing: the award cannot rise at all. + assert childcare[HIGH][i] == childcare[LOW][i], family + assert award[HIGH][i] <= award[LOW][i] + TOLERANCE, family + else: + # The payment starts. It can open the childcare costs element + # (reg. 32(2)(b)) and nothing else. + opened = max(0, childcare[HIGH][i] - childcare[LOW][i]) + assert award[HIGH][i] <= award[LOW][i] + opened + TOLERANCE, family + # Invariant 4: a pound more adds between nothing and a pound to the + # recipient's earned income. + gain = results.recipient(earned, i, HIGH) - results.recipient(earned, i, LOW) + assert gain >= -TOLERANCE, family + assert gain <= family["high"] - family["low"] + TOLERANCE, family + + +def counts_as_wages(family): + """Whether the model taxes the family's payment exactly as it taxes wages. + + It charges no income tax on statutory paternity pay. And + pension_contributions_relief caps relief at employment and self-employment + income, which leaves statutory pay out: a recipient whose wages and profit + are below their pension contributions gets less relief on statutory pay + than on the same amount of wages. + """ + if family["recipient"] >= len(family["adults"]): + return False + recipient = family["adults"][family["recipient"]] + covered = ( + recipient["wages"] + recipient["profit"] >= recipient["pension_contributions"] + ) + return family["payment"] in TAXED_AS_WAGES and covered + + +@PROPERTY_SETTINGS +@given(population=populations(marriage_allowance=True)) +def test_statutory_pay_counts_as_wages_and_earned_income_adds_up(population): + results = Results(population) + for i, family in enumerate(population): + # Invariant 3, for every adult. No adult is 75 or over, the age from + # which contributions stop being relievable. + for member, adult in enumerate(family["adults"]): + paid = family["high"] if member == family["recipient"] else 0.0 + gross = adult["wages"] + paid + max(0, adult["profit"]) + deductions = adult["pension_contributions"] + sum( + results.person(variable, i, HIGH, member) + for variable in [ + "uc_income_tax_on_earnings", + "uc_national_insurance_on_earnings", + ] + ) + np.testing.assert_allclose( + results.person( + "uc_individual_earned_income_before_mif", i, HIGH, member + ), + max(0, gross - deductions), + atol=TOLERANCE, + rtol=1e-6, + err_msg=str(family), + ) + # Invariant 2. + if not counts_as_wages(family): + continue + for variable in [ + "income_tax", + "national_insurance", + "uc_income_tax_on_earnings", + "uc_national_insurance_on_earnings", + "uc_individual_earned_income_before_mif", + "uc_individual_earned_income", + ]: + np.testing.assert_allclose( + results.recipient(variable, i, HIGH), + results.recipient(variable, i, AS_WAGES), + atol=TOLERANCE, + err_msg=f"{variable}: {family}", + ) + for variable in ["uc_earned_income", "universal_credit_pre_benefit_cap"]: + taxed = [counts_as_wages(family) for family in population] + np.testing.assert_allclose( + results.families(variable, HIGH)[taxed], + results.families(variable, AS_WAGES)[taxed], + atol=TOLERANCE, + err_msg=variable, + ) + + +@PROPERTY_SETTINGS +@given(population=populations(marriage_allowance=True, dependant_recipient=True)) +def test_a_dependants_statutory_pay_is_not_the_claimants_earned_income(population): + results = Results(population) + for variable in ["uc_earned_income", "universal_credit"]: + np.testing.assert_array_equal( + results.families(variable, HIGH), + results.families(variable, LOW), + err_msg=variable, + ) + # The young person does have the earnings; they are just not the + # claimants'. + earned = "uc_individual_earned_income_before_mif" + for i, family in enumerate(population): + assert ( + results.recipient(earned, i, HIGH) + >= results.recipient(earned, i, LOW) - TOLERANCE + ), family + + +# Deterministic paths through the 2026-27 thresholds, for each payment. +PATH = [ + 0, + 1, + 100, + 269, + 270, + 271, + 1_000, + 2_000, + 5_000, + 12_569, + 12_570, + 12_571, + 20_000, + 37_700, + 50_270, + 50_271, + 80_000, +] + + +def adult(age=30, wages=0.0, profit=0.0, claims_marriage_allowance=False): + return dict( + age=age, + wages=wages, + profit=profit, + startup=False, + pension_contributions=0.0, + pension_income=0.0, + dividends=0.0, + lcwra=False, + claims_marriage_allowance=claims_marriage_allowance, + ) + + +def family(adults, child_ages=(), rent=0.0, region="NORTH_EAST", childcare=0.0): + return dict( + adults=adults, + child_ages=list(child_ages), + childcare=childcare, + region=region, + tenure="RENT_FROM_COUNCIL", + rent=rent, + recipient=0, + ) + + +ARCHETYPES = { + "single adult with no other income": family([adult()]), + "lone parent on 20,000": family([adult(wages=20_000)], [5], rent=9_600), + "couple on 50,000, crossing the basic rate limit": family( + [adult(40, wages=50_000), adult(38)], [5, 5, 5], rent=36_000 + ), + "couple on 99,000, crossing the personal allowance taper": family( + [adult(40, wages=99_000), adult(38)], [5, 5, 5], rent=36_000 + ), + "Scottish lone parent on 30,000": family( + [adult(wages=30_000)], [5, 5], rent=18_000, region="SCOTLAND" + ), + "self-employed below the minimum income floor": family( + [adult(profit=5_000)], rent=9_600 + ), +} + + +def along_paths(bases, payments, variables): + """Each variable along PATH, for each base family and payment. + + Returns {variable: {version: array[base, payment, step]}}, where HIGH + raises the payment along the path and AS_WAGES raises wages instead. + """ + population = [ + dict(base, payment=payment, low=0.0, high=float(step)) + for base in bases + for payment in payments + for step in PATH + ] + results = Results(population) + shape = (len(bases), len(payments), len(PATH)) + return { + variable: { + version: results.families(variable, version).reshape(shape) + for version in (HIGH, AS_WAGES) + } + for variable in variables + } + + +def test_award_falls_along_a_path_of_rising_statutory_pay(): + paths = along_paths(list(ARCHETYPES.values()), PAYMENTS, ["universal_credit"]) + awards = paths["universal_credit"][HIGH] + for i, name in enumerate(ARCHETYPES): + for j, payment in enumerate(PAYMENTS): + path = awards[i, j] + assert path[0] > 0, name + assert (np.diff(path) <= TOLERANCE).all(), (name, payment, path) + # The path takes the award down, so the test can fail. + assert path[-1] < path[0] - 1_000, (name, payment, path) + + +def test_first_pound_of_statutory_pay_can_open_the_childcare_element(): + # Intended: reg. 32(2)(b) treats a claimant receiving statutory sick, + # maternity or paternity pay as in paid work, which the childcare costs + # element requires (reg. 32(1)(a)). The family has no rent, so the + # benefit cap does not take any of the element back. + base = family([adult()], [5], childcare=6_000) + paths = along_paths([base], PAYMENTS, ["universal_credit", "uc_childcare_element"]) + for j, payment in enumerate(PAYMENTS): + awards = paths["universal_credit"][HIGH][0, j] + childcare = paths["uc_childcare_element"][HIGH][0, j] + assert childcare[0] == 0 + assert childcare[1] > 0 + # The award rises by the element, less at most the pound of pay. + rise = awards[1] - awards[0] + assert childcare[1] - 1 - TOLERANCE <= rise <= childcare[1] + TOLERANCE + # Once the payment is in payment the element does not move, and the + # award never rises again. + assert (childcare[1:] == childcare[1]).all() + assert (np.diff(awards[1:]) <= TOLERANCE).all(), (payment, awards) + + +def test_marriage_allowance_cliff_is_the_same_as_for_wages(): + # Intended: ITA 2007 s. 55B(2)(b) withdraws the whole Marriage Allowance + # reduction once the recipient is liable above the basic rate. The pound + # of pay that takes the recipient past the basic rate limit therefore + # lowers earned income and raises the award, exactly as a pound of wages + # does. The monotonic property above leaves these families out. + base = family( + [ + adult(40, wages=50_000, claims_marriage_allowance=True), + adult(38, claims_marriage_allowance=True), + ], + [5, 5, 5], + rent=36_000, + ) + variables = ["uc_earned_income", "universal_credit_pre_benefit_cap"] + paths = along_paths([base], TAXED_AS_WAGES, variables) + for variable in variables: + np.testing.assert_allclose( + paths[variable][HIGH], + paths[variable][AS_WAGES], + atol=TOLERANCE, + err_msg=variable, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py index 73a93702a1..94cd2ba2a0 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py @@ -9,16 +9,19 @@ class uc_individual_earned_income_before_mif(Variable): ) documentation = ( "The person's actual earned income for Universal Credit: their " - "earnings less their own relievable pension contributions and their " - "own income tax and National Insurance in respect of their employment " + "earnings, including statutory sick, maternity and paternity pay, " + "less their own relievable pension contributions and their own " + "income tax and National Insurance in respect of their employment " "and self-employment. The minimum income floor compares this with " - "the net threshold." + "the net threshold. Regulation 55(4) also treats statutory adoption, " + "shared parental, parental bereavement and neonatal care pay as " + "employed earnings; the model has no variables for those." ) definition_period = YEAR unit = GBP reference = [ dict( - title="Universal Credit Regulations 2013 reg. 55(5)", + title="Universal Credit Regulations 2013 reg. 55(4) and (5)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", ), dict( @@ -35,7 +38,20 @@ def formula(person, period, parameters): # The deductions are the person's own and come off only their own # earnings, so one partner's tax, NI or pension contributions never # reduce the other partner's earned income. - earnings_components = ["employment_income", "miscellaneous_income"] + # Reg. 55(4) treats statutory sick, maternity and paternity pay as + # employed earnings, and reg. 55(5) takes the deductions off "the + # amount of general earnings or benefits specified in paragraphs (2) + # to (4)". Income tax and National Insurance already fall on these + # payments (taxable_employment_income, ni_class_1_income), so leaving + # them out of the earnings deducted the tax on them from other + # earnings and raised the award as the payments rose. + earnings_components = [ + "employment_income", + "miscellaneous_income", + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + ] bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: earnings_components.append("basic_income") From 0dce50c86c0fcb0e139797f03cb1b4f29be45d38 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 22:22:29 -0400 Subject: [PATCH 2/2] Count paternity pay as household income; state the benefit cap exception Review of 70851abb1 (GPT-6.1 Sol) found two blocking problems: - Statutory paternity pay was in no household income list, while statutory sick and maternity pay were. Once Universal Credit tapers it, household net income fell as paternity pay rose (UC -220 on 400 of pay, with the pay never counted). Add it next to maternity pay in household_benefits, both HBAI lists and gov_spending, and add a net income regression. - The monotonic property checked the award after the benefit cap. Reg. 82(1)(a) lifts the cap once earned income reaches the threshold, and statutory pay is earned income, so a correct cap would fail it. Check the award before the cap, and let the final award rise only where the family becomes exempt. Also: prove the Marriage Allowance cliff directly, qualify it for Scottish taxpayers, add a property that each payment counts once in household_benefits, add a YAML case where NI on paternity pay is deducted, and correct the comment and changelog: income tax falls on sick and maternity pay only. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2337.fixed.md | 2 +- .../income/uc_statutory_pay.yaml | 55 ++++++- .../tests/test_uc_statutory_pay_properties.py | 153 +++++++++++++++--- .../uc_individual_earned_income_before_mif.py | 10 +- policyengine_uk/variables/gov/gov_spending.py | 1 + .../household/income/hbai_benefits.py | 1 + .../income/hbai_household_net_income.py | 1 + .../household/income/household_benefits.py | 1 + 8 files changed, 194 insertions(+), 30 deletions(-) diff --git a/changelog.d/2337.fixed.md b/changelog.d/2337.fixed.md index 17d3f13dbf..7d8bb217b2 100644 --- a/changelog.d/2337.fixed.md +++ b/changelog.d/2337.fixed.md @@ -1 +1 @@ -- Counted statutory sick, maternity and paternity pay as Universal Credit earned income (UC Regs 2013 reg. 55(4)). The income tax and National Insurance on them were already deducted from earned income, so Universal Credit rose as the payments rose. +- Counted statutory sick, maternity and paternity pay as Universal Credit earned income (UC Regs 2013 reg. 55(4)). Universal Credit already deducted the National Insurance on all three, and the income tax on sick and maternity pay, so it rose as the payments rose. Statutory paternity pay is now also counted in household benefits, HBAI income and government spending, as sick and maternity pay already were. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml index 910c62df7d..c7c8c2d29f 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_statutory_pay.yaml @@ -7,8 +7,9 @@ # bereavement and neonatal care pay too; the model has no variables for them. # # Before this was fixed the payments were left out of earned income while the -# income tax and National Insurance on them were still deducted. Each pound of -# sick or maternity pay above the thresholds then raised the award by +# National Insurance on them, and the income tax on sick and maternity pay, +# were still deducted (the model charges no income tax on paternity pay). Each +# pound of sick or maternity pay above the thresholds then raised the award by # 0.55 x (20% + 8%) = 15.4p; each pound of paternity pay by 0.55 x 8% = 4.4p. # # Hand calculations use the 2026-27 rates: @@ -201,3 +202,53 @@ # 424.90 x 12 + 351.88 x 12, the higher child element for a first child # born before 6 April 2017. The sick pay takes nothing off it. universal_credit: 9_321.36 + +- name: National Insurance on statutory paternity pay above the threshold comes off earned income + # Employee pension contributions of 8,000 under a net pay arrangement take + # taxable income below the personal allowance, so no income tax is due + # whether or not paternity pay is taxed; National Insurance is charged on + # pay before pension contributions. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + employment_income: 20_000 + employee_pension_contributions: 8_000 + statutory_paternity_pay: 400 + output: + uc_income_tax_on_earnings: 0 + # (20,000 + 400 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: 626.40 + # 20,400 - 8,000 relievable pension contributions - 626.40 (reg. 55(5)). + uc_individual_earned_income: 11_773.60 + +- name: Statutory paternity pay is household income, so it never lowers net income through Universal Credit + # Statutory sick and maternity pay were already in household benefits, + # HBAI income and government spending; paternity pay was in none of them. + # Once Universal Credit tapers it, leaving it out would make net income + # fall by 0.55 x 400 = 220 as the pay rose. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + statutory_paternity_pay: 400 + benunits: + benunit: + members: [parent] + households: + household: + members: [parent] + council_tax: 0 + tv_licence: 0 + expected_sdlt: 0 + output: + # 424.90 x 12 - 0.55 x 400, with no work allowance. + universal_credit: 4_878.80 + # The award plus the pay. Without the pay the household has 5,098.80, so + # 400 of pay raises its income by 180. + household_benefits: 5_278.80 + household_net_income: 5_278.80 + hbai_household_net_income: 5_278.80 + gov_spending: 5_278.80 diff --git a/policyengine_uk/tests/test_uc_statutory_pay_properties.py b/policyengine_uk/tests/test_uc_statutory_pay_properties.py index f1f84036c1..ef2de488c3 100644 --- a/policyengine_uk/tests/test_uc_statutory_pay_properties.py +++ b/policyengine_uk/tests/test_uc_statutory_pay_properties.py @@ -11,14 +11,16 @@ pay (employment_benefits in taxable_employment_income), as it does on wages. Before this was fixed the payments were left out of earned income while the -tax and National Insurance on them were still deducted, so the award rose by -the taper rate times that tax as a payment rose. +National Insurance on them, and the income tax on sick and maternity pay, were +still deducted, so the award rose by the taper rate times that tax as a +payment rose. Invariants, for any generated population of single claimants and couples, with or without children, in which one member receives one of the three payments: 1. Monotonic: raising a claimant's payment, with everything else fixed, never - raises the award. Two rules of law make exceptions, and neither is a fault: + raises the award before the benefit cap. Three rules of law make + exceptions, and none is a fault: a. Reg. 32(2)(b) treats a claimant receiving statutory sick, maternity or paternity pay as in paid work for the childcare costs element. So the @@ -27,13 +29,21 @@ and only as the payment starts from nil (test_first_pound_of_statutory_pay_can_open_the_childcare_element). b. Income Tax Act 2007 s. 55B(2)(b) withdraws the Marriage Allowance tax - reduction in full once the recipient is liable at a rate above the - basic rate. A pound of pay that crosses that line raises the tax on the - earnings by more than a pound, earned income falls and the award rises. - Wages do the same + reduction in full once the recipient is liable at a rate the subsection + does not list (it lists the basic rate and, for a Scottish taxpayer, the + Scottish rates up to the intermediate rate). A pound of pay that crosses + that line raises the tax on the earnings by more than a pound, earned + income falls and the award rises. Wages do the same (test_marriage_allowance_cliff_is_the_same_as_for_wages), so the monotonic property is stated for families that do not claim Marriage Allowance. + c. The benefit cap does not apply where earned income reaches the reg. + 82(1)(a) threshold, and the payments are earned income (reg. 55(4)). So + a pound of pay can lift the cap and raise the final award. The award + after the cap may therefore rise only where the family becomes exempt + (is_benefit_cap_exempt); otherwise it obeys the same bound. Today's + is_benefit_cap_exempt_earnings reads employment income without + statutory pay, so this exception does not yet arise in the model. 2. Same as wages: statutory sick or maternity pay of s on top of wages of e gives the same income tax, National Insurance, earned income and award @@ -55,6 +65,12 @@ recipient's earned income, for families that do not claim Marriage Allowance. +6. Household income: each payment counts once in household_benefits, and so + in household net income. Raising it by d raises household_benefits by d + plus the change in the other benefits listed there. Statutory paternity + pay was in no household income list, so once Universal Credit tapered it + household net income fell as the pay rose. + 5. A dependant's payment changes neither the claimants' earned income nor the award (reg. 22(1)(b): "the claimant's earned income (or, in the case of joint claimants, their combined earned income)"). @@ -68,7 +84,14 @@ from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st -from policyengine_uk import Simulation +from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.variables.gov.gov_spending import GOV_SPENDING_VARIABLES +from policyengine_uk.variables.household.income.hbai_household_net_income import ( + HBAI_HOUSEHOLD_NET_INCOME_ADDS, +) +from policyengine_uk.variables.household.income.household_benefits import ( + HOUSEHOLD_BENEFIT_VARIABLES, +) YEAR = 2026 PROPERTY_SETTINGS = settings( @@ -85,6 +108,7 @@ ] # The model charges income tax on these, as on wages (employment_benefits). TAXED_AS_WAGES = ["statutory_sick_pay", "statutory_maternity_pay"] +BENEFITS = list(dict.fromkeys(HOUSEHOLD_BENEFIT_VARIABLES)) # Wages and payments on and around the 2026-27 thresholds: the personal # allowance and primary threshold (12,570), the basic rate limit (50,270) and # the personal allowance taper (100,000 to 125,140). @@ -226,6 +250,13 @@ def families(self, variable, version): values = np.array(self.simulation.calculate(variable, YEAR), dtype=float) return values.reshape(len(self.population), len(VERSIONS))[:, version] + def households(self, variable, version): + """Any variable summed to the household, for one version of every family.""" + values = np.array( + self.simulation.calculate(variable, YEAR, map_to="household"), dtype=float + ) + return values.reshape(len(self.population), len(VERSIONS))[:, version] + def person(self, variable, i, version, member): values = np.array(self.simulation.calculate(variable, YEAR), dtype=float) return values[self.first_member[i, version] + member] @@ -238,25 +269,66 @@ def recipient(self, variable, i, version): @given(population=populations()) def test_award_never_rises_with_a_claimants_statutory_pay(population): results = Results(population) - award = {v: results.families("universal_credit", v) for v in (LOW, HIGH)} - childcare = {v: results.families("uc_childcare_element", v) for v in (LOW, HIGH)} + + def both(variable): + return {v: results.families(variable, v) for v in (LOW, HIGH)} + + before_cap = both("universal_credit_pre_benefit_cap") + award = both("universal_credit") + childcare = both("uc_childcare_element") + exempt = both("is_benefit_cap_exempt") earned = "uc_individual_earned_income_before_mif" for i, family in enumerate(population): if family["low"] > 0: # The payment is already in payment, so reg. 32(2)(b) changes - # nothing: the award cannot rise at all. + # nothing. assert childcare[HIGH][i] == childcare[LOW][i], family - assert award[HIGH][i] <= award[LOW][i] + TOLERANCE, family - else: - # The payment starts. It can open the childcare costs element - # (reg. 32(2)(b)) and nothing else. - opened = max(0, childcare[HIGH][i] - childcare[LOW][i]) + # Exception a: a payment that starts can open the childcare costs + # element, and nothing else. + opened = max(0, childcare[HIGH][i] - childcare[LOW][i]) + assert before_cap[HIGH][i] <= before_cap[LOW][i] + opened + TOLERANCE, family + # Exception c: more earned income never ends an exemption from the + # cap, and only a new exemption can raise the award after the cap. + assert exempt[HIGH][i] >= exempt[LOW][i], family + if exempt[HIGH][i] == exempt[LOW][i]: assert award[HIGH][i] <= award[LOW][i] + opened + TOLERANCE, family # Invariant 4: a pound more adds between nothing and a pound to the # recipient's earned income. gain = results.recipient(earned, i, HIGH) - results.recipient(earned, i, LOW) assert gain >= -TOLERANCE, family assert gain <= family["high"] - family["low"] + TOLERANCE, family + # Invariant 6: each payment is in household_benefits once. + for payment in PAYMENTS: + rows = np.array([family["payment"] == payment for family in population]) + if not rows.any(): + continue + + def change(variable): + return results.households(variable, HIGH) - results.households( + variable, LOW + ) + + others = sum(change(benefit) for benefit in BENEFITS if benefit != payment) + paid = np.array([family["high"] - family["low"] for family in population]) + np.testing.assert_allclose( + (change("household_benefits") - others)[rows], + paid[rows], + atol=TOLERANCE, + rtol=1e-5, + err_msg=payment, + ) + + +def test_each_payment_is_household_income_and_government_spending(): + lists = { + "HOUSEHOLD_BENEFIT_VARIABLES": HOUSEHOLD_BENEFIT_VARIABLES, + "HBAI_HOUSEHOLD_NET_INCOME_ADDS": HBAI_HOUSEHOLD_NET_INCOME_ADDS, + "hbai_benefits.adds": CountryTaxBenefitSystem().variables["hbai_benefits"].adds, + "GOV_SPENDING_VARIABLES": GOV_SPENDING_VARIABLES, + } + for name, listed in lists.items(): + for payment in PAYMENTS: + assert listed.count(payment) == 1, (name, payment) def counts_as_wages(family): @@ -440,15 +512,25 @@ def along_paths(bases, payments, variables): def test_award_falls_along_a_path_of_rising_statutory_pay(): - paths = along_paths(list(ARCHETYPES.values()), PAYMENTS, ["universal_credit"]) - awards = paths["universal_credit"][HIGH] + variables = [ + "universal_credit_pre_benefit_cap", + "universal_credit", + "is_benefit_cap_exempt", + ] + paths = along_paths(list(ARCHETYPES.values()), PAYMENTS, variables) for i, name in enumerate(ARCHETYPES): for j, payment in enumerate(PAYMENTS): - path = awards[i, j] + path = paths["universal_credit_pre_benefit_cap"][HIGH][i, j] assert path[0] > 0, name assert (np.diff(path) <= TOLERANCE).all(), (name, payment, path) # The path takes the award down, so the test can fail. assert path[-1] < path[0] - 1_000, (name, payment, path) + # After the cap, the award rises only at a step that makes the + # family exempt (reg. 82(1)(a)). + final = paths["universal_credit"][HIGH][i, j] + newly_exempt = np.diff(paths["is_benefit_cap_exempt"][HIGH][i, j]) > 0 + rises = np.diff(final) > TOLERANCE + assert not (rises & ~newly_exempt).any(), (name, payment, final) def test_first_pound_of_statutory_pay_can_open_the_childcare_element(): @@ -474,10 +556,11 @@ def test_first_pound_of_statutory_pay_can_open_the_childcare_element(): def test_marriage_allowance_cliff_is_the_same_as_for_wages(): # Intended: ITA 2007 s. 55B(2)(b) withdraws the whole Marriage Allowance - # reduction once the recipient is liable above the basic rate. The pound - # of pay that takes the recipient past the basic rate limit therefore - # lowers earned income and raises the award, exactly as a pound of wages - # does. The monotonic property above leaves these families out. + # reduction once the recipient (an English taxpayer here) is liable above + # the basic rate. The pound of pay that takes the recipient past the + # basic rate limit therefore lowers earned income and raises the award, + # exactly as a pound of wages does. The monotonic property above leaves + # these families out. base = family( [ adult(40, wages=50_000, claims_marriage_allowance=True), @@ -495,3 +578,27 @@ def test_marriage_allowance_cliff_is_the_same_as_for_wages(): atol=TOLERANCE, err_msg=variable, ) + # The cliff is there: the reduction goes in one step, and at that step + # earned income falls and the award rises. + population = [ + dict(base, payment=payment, low=0.0, high=float(step)) + for payment in TAXED_AS_WAGES + for step in PATH + ] + results = Results(population) + shape = (len(TAXED_AS_WAGES), len(PATH)) + reduction = np.array( + [ + results.recipient("marriage_allowance", i, HIGH) + for i in range(len(population)) + ] + ).reshape(shape) + for j, payment in enumerate(TAXED_AS_WAGES): + assert reduction[j, 0] > 0 and reduction[j, -1] == 0, (payment, reduction[j]) + steps = np.flatnonzero(np.diff(reduction[j]) < 0) + assert len(steps) == 1, (payment, reduction[j]) + step = steps[0] + earned = paths["uc_earned_income"][HIGH][0, j] + award = paths["universal_credit_pre_benefit_cap"][HIGH][0, j] + assert earned[step + 1] < earned[step], (payment, earned) + assert award[step + 1] > award[step], (payment, award) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py index 94cd2ba2a0..75d6669a3b 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py @@ -41,10 +41,12 @@ def formula(person, period, parameters): # Reg. 55(4) treats statutory sick, maternity and paternity pay as # employed earnings, and reg. 55(5) takes the deductions off "the # amount of general earnings or benefits specified in paragraphs (2) - # to (4)". Income tax and National Insurance already fall on these - # payments (taxable_employment_income, ni_class_1_income), so leaving - # them out of the earnings deducted the tax on them from other - # earnings and raised the award as the payments rose. + # to (4)". National Insurance falls on all three (ni_class_1_income) + # and income tax on sick and maternity pay (employment_benefits in + # taxable_employment_income), and both are deducted below. Leaving + # the payments out of the earnings deducted the tax and National + # Insurance on them from other earnings, so the award rose as the + # payments rose. earnings_components = [ "employment_income", "miscellaneous_income", diff --git a/policyengine_uk/variables/gov/gov_spending.py b/policyengine_uk/variables/gov/gov_spending.py index ca27cf2a72..bc8c8e6e53 100644 --- a/policyengine_uk/variables/gov/gov_spending.py +++ b/policyengine_uk/variables/gov/gov_spending.py @@ -27,6 +27,7 @@ "maternity_allowance", "statutory_sick_pay", "statutory_maternity_pay", + "statutory_paternity_pay", "ssmg", "basic_income", "epg_subsidy", diff --git a/policyengine_uk/variables/household/income/hbai_benefits.py b/policyengine_uk/variables/household/income/hbai_benefits.py index ac8d146133..010155a879 100644 --- a/policyengine_uk/variables/household/income/hbai_benefits.py +++ b/policyengine_uk/variables/household/income/hbai_benefits.py @@ -32,6 +32,7 @@ class hbai_benefits(Variable): "maternity_allowance", "statutory_sick_pay", "statutory_maternity_pay", + "statutory_paternity_pay", "ssmg", "cost_of_living_support_payment", "winter_fuel_allowance", diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 5061ab23c1..936ebea59a 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -42,6 +42,7 @@ "maternity_allowance", "statutory_sick_pay", "statutory_maternity_pay", + "statutory_paternity_pay", "ssmg", "cost_of_living_support_payment", "winter_fuel_allowance", diff --git a/policyengine_uk/variables/household/income/household_benefits.py b/policyengine_uk/variables/household/income/household_benefits.py index 1de62cbaec..81c86e6072 100644 --- a/policyengine_uk/variables/household/income/household_benefits.py +++ b/policyengine_uk/variables/household/income/household_benefits.py @@ -27,6 +27,7 @@ "maternity_allowance", "statutory_sick_pay", "statutory_maternity_pay", + "statutory_paternity_pay", "ssmg", "basic_income", "epg_subsidy",