Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c07c4e2
Deduct only each person's own tax and NI on earnings from UC earned i…
MaxGhenis Sep 30, 2026
d0fc22f
Add UC earnings-deduction property tests; widen State Pension properties
MaxGhenis Sep 30, 2026
6ec7179
Model Marriage Allowance as the statutory s.55B transfer
MaxGhenis Sep 30, 2026
ba72b98
Test Marriage Allowance from the statute and re-enable it in the UC p…
MaxGhenis Sep 30, 2026
144195c
Add Marriage Allowance property tests, including a brute-force differ…
MaxGhenis Sep 30, 2026
a1a4822
Take the couple's Marriage Allowance take-up from the gaining spouse
MaxGhenis Sep 30, 2026
20c0231
Test dividends against the basic rate limit; fix Marriage Allowance t…
MaxGhenis Sep 30, 2026
6084d7b
Correct Marriage Allowance tests and notes from the statutory review
MaxGhenis Sep 30, 2026
548d6f9
Stack dividends above all non-savings income in the Marriage Allowanc…
MaxGhenis Sep 30, 2026
292f7c8
Harden the Marriage Allowance election from the robustness review
MaxGhenis Oct 1, 2026
514132f
Fix the UC docs means-test sentence; point the Marriage Allowance pin…
MaxGhenis Oct 1, 2026
c98c53c
Merge #1943's head ce85220a (main merged in) into the #1942 fix
MaxGhenis Oct 1, 2026
f333e17
Take tax reductions off the tax on earnings first; deduct only reliev…
MaxGhenis Oct 1, 2026
f0f150d
Generate working pensioners with a tax reduction in the UC earnings p…
MaxGhenis Oct 1, 2026
b8555a2
Follow the tax engine's exclusion list when slicing earnings
MaxGhenis Oct 1, 2026
84817c3
Merge main (#1943 merged, #1959 and others) into the #1942 fix
MaxGhenis Oct 2, 2026
19a7235
Remove main's #1942 xfail on Carer Support Payment; pin the tests rev…
MaxGhenis Oct 2, 2026
d399177
Merge main (#1896 and others) into the #1942 fix
MaxGhenis Oct 2, 2026
a57caf1
Declare the claimant and partner in the UC earnings and State Pension…
MaxGhenis Oct 2, 2026
414a9cd
Merge main (#1950, #1881, #1939, #1944 and others) into the #1942 fix
MaxGhenis Oct 3, 2026
6856804
Merge main (bfc5fea1) into the #1942 fix
MaxGhenis Oct 3, 2026
295ca1e
Update two UC earnings YAML cases for #1950 and #1881 on main
MaxGhenis Oct 3, 2026
c6412ee
Compare the capital-income award invariance only where allowances are…
MaxGhenis Oct 3, 2026
182996d
Merge remote-tracking branch 'origin/main' into uc-earned-income-own-tax
MaxGhenis Oct 3, 2026
8c88c7c
Compare the capped award only where the benefit cap exemption is unch…
MaxGhenis Oct 3, 2026
cf5f1a0
Merge #1949 (uc-earned-income-own-tax, 8c88c7cd) into marriage-allowa…
MaxGhenis Oct 3, 2026
cb0f0ab
Merge origin/main (2cbf20ce) into marriage-allowance-election
MaxGhenis Oct 3, 2026
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/marriage-allowance-election.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Model Marriage Allowance as ITA 2007 ss. 55A-55E provide. The electing spouse gives up the fixed transferable amount (10% of the personal allowance, rounded up to £10) from their own allowance, and the other spouse's tax falls at Step 6 by the basic rate (or Scottish basic rate) times that amount, capped at their tax. Previously the recipient's taxable income fell by the transferor's unused allowance. The couple elects only when the conditions in s. 55B(2) and s. 55C(1) are met and it lowers their combined income tax. Universal Credit deducts the reduction from the recipient's tax on their earnings, so a pensioner's State Pension no longer changes the earning partner's earned income.
1 change: 1 addition & 0 deletions changelog.d/uc-earned-income-own-tax.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deduct from each person's Universal Credit earnings only their own income tax and National Insurance on that employment or self-employment (UC Regs 2013 reg. 55(5)(b), reg. 57(2) step 3), not the whole benefit unit's tax on all income, so tax on pensions, State Pension, property, savings and dividends, the High Income Child Benefit Charge and voluntary Class 3 contributions no longer reduce earned income.
2 changes: 1 addition & 1 deletion docs/book/programs/gov/dwp/universal-credit.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
{
"cell_type": "markdown",
"metadata": {},
"source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter, currently 55%), plus all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`."
"source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the benunit's combined earnings above its work allowance (`uc_work_allowance`), and by all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`."
},
{
"cell_type": "code",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
description: Maximum Marriage Allowance taxable income reduction, as a percentage of the full Personal Allowance.
description: Marriage Allowance transferable amount, as a share of the personal allowance in ITA 2007 s. 35(1). For 2015-16 the Act fixes the amount at £1,060, which is 10% of that year's £10,600 allowance.
values:
2016-04-01: 0.1
2015-04-06: 0.1
metadata:
unit: /1
label: Marriage Allowance maximum percentage
label: Marriage Allowance transferable share of the personal allowance
reference:
- title: Income Tax Act 2007 s. 55B(4)(b)
href: https://www.legislation.gov.uk/ukpga/2007/3/part/3/chapter/3A
- title: Income Tax Act 2007 s. 55B(4)
href: https://www.legislation.gov.uk/ukpga/2007/3/section/55B
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
description: The Marriage Allowance is rounded up by this increment.
description: The Marriage Allowance transferable amount is rounded up to a multiple of this amount.
values:
2016-04-01: 10
metadata:
unit: currency-GBP
label: Marriage Allowance rounding increment
reference:
- title: Income Tax Act 2007 s. 55B(5)
href: https://www.legislation.gov.uk/ukpga/2007/3/part/3/chapter/3A
href: https://www.legislation.gov.uk/ukpga/2007/3/section/55B
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ values:
- taxable_dividend_income
- taxable_property_income
- received_allowances_earned_income
- marriage_allowance

metadata:
unit: list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ metadata:
reference:
- title: Income Tax Act 2007 s. 23
href: https://www.legislation.gov.uk/ukpga/2007/3/section/23
- title: Income Tax Act 2007 s. 26(1)(a)
href: https://www.legislation.gov.uk/ukpga/2007/3/section/26
values:
0000-01-01:
- capped_mcad
- other_tax_credits
- marriage_allowance_tax_reduction
76 changes: 33 additions & 43 deletions policyengine_uk/reforms/cps/marriage_tax_reforms.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from policyengine_uk.model_api import *
from policyengine_uk.variables.gov.hmrc.income_tax.allowances.meets_marriage_allowance_income_conditions import (
liable_only_at_marriage_allowance_rates,
)
from typing import Union, Optional


def create_expanded_ma_reform(
max_child_age: Optional[int] = None,
child_education_levels: Optional[List[str]] = None,
remove_income_condition: bool = True,
) -> Reform:
class meets_expanded_ma_conditions(Variable):
label = "Qualifies for an expanded Marriage Allowance"
Expand Down Expand Up @@ -32,70 +36,54 @@ def formula(person, period):

class meets_marriage_allowance_income_conditions(Variable):
label = "Meets Marriage Allowance income conditions"
documentation = "Whether this person (and their partner) meets the conditions for this person to be eligible for the Marriage Allowance, as set out in the Income Tax Act 2007 sections 55B and 55C"
documentation = (
"Whether this person pays income tax only at the rates that allow "
"a Marriage Allowance election, or meets the reform's expansion "
"conditions, which lift that restriction."
)
entity = Person
definition_period = YEAR
value_type = bool
reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B"

def formula(person, period, parameters):
band = person("tax_band", period)
eligible_bands = parameters(
period
).gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands
base_eligible = np.isin(band.decode_to_str(), eligible_bands)
# Expand to higher bands if the reform's expansion conditions are met.
bands = band.possible_values
expansion = person("meets_expanded_ma_conditions", period) & (
(band == bands.HIGHER) | (band == bands.ADDITIONAL)
conditions = liable_only_at_marriage_allowance_rates(
person, period, parameters
)
return base_eligible | expansion
if not remove_income_condition:
return conditions
return conditions | person("meets_expanded_ma_conditions", period)

class marriage_allowance(Variable):
class marriage_allowance_transferable_amount(Variable):
value_type = float
entity = Person
label = "Marriage Allowance"
label = "Marriage Allowance transferable amount"
documentation = (
"The share of the personal allowance a Marriage Allowance election "
"transfers, at the reform's rate for couples meeting its expansion "
"conditions, rounded up to the rounding increment."
)
definition_period = YEAR
reference = "https://www.legislation.gov.uk/ukpga/2007/3/part/3/chapter/3A"
reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B"
unit = GBP

def formula(person, period, parameters):
marital = person("marital_status", period)
married = marital == marital.possible_values.MARRIED
eligible = (
married
& person("is_claimant_or_partner", period)
& person("meets_marriage_allowance_income_conditions", period)
)
transferable_amount = person("partners_unused_personal_allowance", period)
allowances = parameters(period).gov.hmrc.income_tax.allowances
capped_percentage = allowances.marriage_allowance.max
expanded_ma_cap = parameters(
period
).gov.contrib.cps.marriage_tax_reforms.expanded_ma.ma_rate
capped_percentage = where(
p = parameters(period)
allowances = p.gov.hmrc.income_tax.allowances
share = where(
person("meets_expanded_ma_conditions", period),
expanded_ma_cap,
capped_percentage,
)
max_amount = allowances.personal_allowance.amount * capped_percentage
amount_if_eligible_pre_rounding = max_(
min_(transferable_amount, max_amount), 0
)
# Round up.
rounding_increment = allowances.marriage_allowance.rounding_increment
amount_if_eligible = (
np.ceil(amount_if_eligible_pre_rounding / rounding_increment)
* rounding_increment
p.gov.contrib.cps.marriage_tax_reforms.expanded_ma.ma_rate,
allowances.marriage_allowance.max,
)
takes_up = person("would_claim_marriage_allowance", period)
return eligible * amount_if_eligible * takes_up
amount = allowances.personal_allowance.amount * share
increment = allowances.marriage_allowance.rounding_increment
return np.ceil(amount / increment) * increment

class reform(Reform):
def apply(self):
self.add_variable(meets_expanded_ma_conditions)
self.update_variable(meets_marriage_allowance_income_conditions)
self.update_variable(marriage_allowance)
self.update_variable(marriage_allowance_transferable_amount)

return reform

Expand Down Expand Up @@ -220,6 +208,7 @@ def create_marriage_tax_reform(parameters, period):
if remove_income_condition or rate != original_rate:
ma_reform = create_expanded_ma_reform(
max_child_age=ma_max_child_age if ma_max_child_age > 0 else None,
remove_income_condition=bool(remove_income_condition),
)
else:
ma_reform = None
Expand All @@ -246,4 +235,5 @@ def create_marriage_tax_reform(parameters, period):
# These build the reform classes with no child-age / education conditions, so the reform
# applies to every married couple regardless of child presence.
expanded_ma_reform = create_expanded_ma_reform()
expanded_ma_reform_rate_only = create_expanded_ma_reform(remove_income_condition=False)
marriage_neutral_it_reform = create_marriage_neutral_income_tax_reform()
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
employment_income: 1_000
miscellaneous_income: 100
uc_work_allowance: 10
# The claimant's own tax (UC reg 22 counts the claimant's earnings).
tax: 1
# The claimant's own tax on these earnings.
uc_income_tax_on_earnings: 1
output:
uc_earned_income: 1100 - 11
uc_individual_earned_income: 1_100 - 1
uc_earned_income: 1_100 - 11

- name: Capped at 0
period: 2025
Expand All @@ -17,7 +18,46 @@
employment_income: 1_000
miscellaneous_income: 100
uc_work_allowance: 2_000
# The claimant's own tax (UC reg 22 counts the claimant's earnings).
tax: 1
# The claimant's own tax on these earnings.
uc_income_tax_on_earnings: 1
output:
uc_earned_income: 0

- name: Each person's deductions come off only their own earnings
period: 2025
absolute_error_margin: 0
input:
people:
earner:
employment_income: 1_000
uc_income_tax_on_earnings: 100
uc_national_insurance_on_earnings: 10
partner:
uc_income_tax_on_earnings: 500
pension_contributions: 50
benunits:
benunit:
members: [earner, partner]
uc_work_allowance: 0
output:
uc_individual_earned_income: [890, 0]
uc_earned_income: 890

- name: Work allowance comes off combined earnings after each person's deductions
period: 2025
absolute_error_margin: 0
input:
people:
earner:
employment_income: 1_000
uc_income_tax_on_earnings: 100
partner:
employment_income: 500
pension_contributions: 50
benunits:
benunit:
members: [earner, partner]
uc_work_allowance: 300
output:
uc_individual_earned_income: [900, 450]
uc_earned_income: 1_050
Loading
Loading