diff --git a/changelog.d/marriage-allowance-election.fixed.md b/changelog.d/marriage-allowance-election.fixed.md new file mode 100644 index 000000000..d5f17e005 --- /dev/null +++ b/changelog.d/marriage-allowance-election.fixed.md @@ -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. diff --git a/changelog.d/uc-earned-income-own-tax.fixed.md b/changelog.d/uc-earned-income-own-tax.fixed.md new file mode 100644 index 000000000..0a800b871 --- /dev/null +++ b/changelog.d/uc-earned-income-own-tax.fixed.md @@ -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. diff --git a/changelog.d/uc-state-pension-unearned-income.fixed.md b/changelog.d/uc-state-pension-unearned-income.fixed.md new file mode 100644 index 000000000..caa0e2969 --- /dev/null +++ b/changelog.d/uc-state-pension-unearned-income.fixed.md @@ -0,0 +1 @@ +Count State Pension as Universal Credit unearned income (retirement pension income, UC Regs 2013 regs 66(1)(a) and 67). Mixed-age couples on UC were having their State Pension ignored. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 5581288a9..c87f8f251 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -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 its unearned income. 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**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate. 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 combined earnings above the work allowance are then tapered (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, contribution-based JSA, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. 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", diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml index 489abe369..29e83eb13 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml @@ -8,6 +8,10 @@ metadata: reference: - title: The Universal Credit Regulations 2013 reg. 66 href: https://www.legislation.gov.uk/uksi/2013/376/regulation/66 + - title: The Universal Credit Regulations 2013 reg. 67 (retirement pension income) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/67 + - title: State Pension Credit Act 2002 s. 16(1)(za), (a) (State Pension counts as retirement pension income) + href: https://www.legislation.gov.uk/ukpga/2002/16/section/16 - title: The Universal Credit Regulations 2013 reg. 72 href: https://www.legislation.gov.uk/uksi/2013/376/regulation/72 @@ -17,6 +21,7 @@ values: - carers_allowance - jsa_contrib - private_pension_income + - state_pension - uc_tariff_income - savings_interest_income - dividend_income diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml deleted file mode 100644 index 526252c4e..000000000 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/eligible_bands.yaml +++ /dev/null @@ -1,14 +0,0 @@ -description: Tax bands whose members may transfer personal allowance under the Marriage Allowance election (ITA 2007 s.55B). A transferor is eligible if they are not liable at the higher rate or above; the Scottish intermediate rate is treated as within the basic-rate envelope for this purpose. -values: - 0000-01-01: - - NONE - - STARTER - - BASIC - - INTERMEDIATE -metadata: - unit: list - label: Marriage Allowance eligible tax bands - period: year - reference: - - title: "Income Tax Act 2007, s. 55B" - href: https://www.legislation.gov.uk/ukpga/2007/3/section/55B diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml index bbde24b1b..21e32ebf9 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/max.yaml @@ -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 diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/minimum_spouse_age.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/minimum_spouse_age.yaml new file mode 100644 index 000000000..772633627 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/minimum_spouse_age.yaml @@ -0,0 +1,11 @@ +description: Youngest age at which anyone in the UK can be a party to a marriage, and so a spouse who can make or gain from a Marriage Allowance election. It is 16 in Scotland; England and Wales raised it to 18 from 27 February 2023. +values: + 0000-01-01: 16 +metadata: + unit: year + label: Minimum age of a Marriage Allowance spouse + reference: + - title: Marriage (Scotland) Act 1977 s. 1(1) + href: https://www.legislation.gov.uk/ukpga/1977/15/section/1 + - title: Marriage and Civil Partnership (Minimum Age) Act 2022 s. 1 + href: https://www.legislation.gov.uk/ukpga/2022/28/section/1 diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/rounding_increment.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/rounding_increment.yaml index af43fa56b..c396ce4e4 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/rounding_increment.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/marriage_allowance/rounding_increment.yaml @@ -1,4 +1,4 @@ -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: @@ -6,4 +6,4 @@ metadata: 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 diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml index d40254f49..f4e83a3d2 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/earned_taxable_income_exclusions.yaml @@ -5,7 +5,6 @@ values: - taxable_dividend_income - taxable_property_income - received_allowances_earned_income - - marriage_allowance metadata: unit: list diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml index b516d33e3..2fae722e0 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml @@ -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 diff --git a/policyengine_uk/reforms/cps/marriage_tax_reforms.py b/policyengine_uk/reforms/cps/marriage_tax_reforms.py index c10ff49f3..a3d49a40f 100644 --- a/policyengine_uk/reforms/cps/marriage_tax_reforms.py +++ b/policyengine_uk/reforms/cps/marriage_tax_reforms.py @@ -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" @@ -28,66 +32,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( - "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 = min_(transferable_amount, max_amount) - # 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 @@ -206,6 +198,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 @@ -232,4 +225,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() diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml index 3647f0e3d..378941b5d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml @@ -5,9 +5,10 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 10 - benunit_tax: 1 + 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 @@ -16,6 +17,45 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 2_000 - benunit_tax: 1 + 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 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml new file mode 100644 index 000000000..4b24733ac --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml @@ -0,0 +1,249 @@ +# Universal Credit deducts from a person's earnings only the income tax and +# National Insurance that person pays in respect of their own employment +# (UC Regs 2013 reg. 55(5)(b)) or trade (reg. 57(2) step 3), plus their own +# relievable pension contributions (reg. 55(5)(a)). Tax on pensions, State +# Pension, property, savings and dividends is not deducted, and one partner's +# tax never reduces the other's earnings. Earnings are taken as the lowest +# slice of the person's non-savings income, after their allowances; savings +# and dividends sit above all non-savings income (ITA 2007 s. 16). +# +# Hand calculations use the 2026-27 rates: +# - personal allowance 12,570; basic rate 20% on the first 37,700 of taxable +# income; dividend allowance 500, dividend ordinary rate 10.75%; property +# allowance 1,000; trading allowance 1,000; +# - primary Class 1 NI 8% above the primary threshold of 241.73 a week +# (12,569.96 a year); Class 4 NI 6% above the lower profits limit of 12,570; +# Class 2 NI nil; +# - 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: Single parent with earnings and property income, only tax on earnings is deducted + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + property_income: 5_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + # Earnings take the personal allowance first: (20,000 - 12,570) x 20%. + # Property income is taxed on (5,000 - 1,000) x 20% = 800, not deducted. + income_tax: [2_286, 0] + uc_income_tax_on_earnings: [1_486, 0] + # (20,000 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: [594.40, 0] + # 20,000 - 1,486 - 594.40. + uc_individual_earned_income: [17_919.60, 0] + # Less the work allowance of 427 x 12 = 5,124. + uc_earned_income: 12_795.60 + uc_unearned_income: 5_000 + # 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 12,795.60 + 5,000). Deducting all of the tax, as + # before this fix, gave 6,748.50: 0.55 x 800 = 440 too much. + universal_credit: 6_308.50 + +- name: Mixed-age couple, the pensioner's tax on State Pension does not reduce the partner's earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 16_000 + partner: + age: 45 + employment_income: 13_000 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 20_000 + output: + # Pensioner: (16,000 - 12,570) x 20% = 686, on State Pension. + # Partner: (13,000 - 12,570) x 20% = 86 and (13,000 - 12,569.96) x 8% + # = 34.40. Either spouse electing for Marriage Allowance would cost them + # 252 and save the other 252, so the couple does not elect. + income_tax: [686, 86] + uc_income_tax_on_earnings: [0, 86] + uc_national_insurance_on_earnings: [0, 34.40] + # 13,000 - 86 - 34.40. + uc_earned_income: 12_879.60 + uc_unearned_income: 16_000 + # 666.97 x 12 + 20,000. + uc_maximum_amount: 28_003.64 + # 28,003.64 - (0.55 x 12,879.60 + 16,000). Deducting the whole benefit + # unit's tax, as before this fix, gave 5,297.16: 0.55 x 686 = 377.30 + # too much. + universal_credit: 4_919.86 + +- name: Self-employed claimant with pension and dividend income, only tax on the trade is deducted + period: 2026 + absolute_error_margin: 0.01 + input: + age: 55 + self_employment_income: 18_000 + # In a start-up period, so the minimum income floor does not apply. + uc_is_in_startup_period: true + private_pension_income: 4_000 + dividend_income: 3_000 + tenure_type: RENT_FROM_COUNCIL + rent: 12_000 + output: + # Trade: taxable profit 18,000 - 1,000 trading allowance = 17,000; it + # takes the personal allowance first, (17,000 - 12,570) x 20% = 886. + # Pension: 4,000 x 20% = 800. Dividends: (3,000 - 500) x 10.75% = + # 268.75. Neither is deducted. + income_tax: 1_954.75 + uc_income_tax_on_earnings: 886 + # Class 4: (18,000 - 12,570) x 6%. + uc_national_insurance_on_earnings: 325.80 + # 18,000 - 886 - 325.80, with no work allowance. + uc_earned_income: 16_788.20 + uc_unearned_income: 7_000 + # 424.90 x 12 + 12,000. + uc_maximum_amount: 17_098.80 + # 17,098.80 - (0.55 x 16,788.20 + 7,000). Deducting all of the tax and + # NI, as before this fix, gave 1,453.10: 0.55 x 1,068.75 = 587.81 too + # much. + universal_credit: 865.29 + +- name: Scottish taxpayer, earnings sit below pension income in the Scottish bands + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 20_000 + private_pension_income: 10_000 + region: SCOTLAND + output: + # 2026-27 Scottish bands above the personal allowance: starter 19% on the + # first 3,967, basic 20% to 16,956, intermediate 21% to 31,092. + # Earnings slice 7,430: 3,967 x 19% + 3,463 x 20% = 753.73 + 692.60. + uc_income_tax_on_earnings: 1_446.33 + # Pension slice 7,430 to 17,430: 9,526 x 20% + 474 x 21% = 2,004.74. + income_tax: 3_451.07 + +- name: Couple with Marriage Allowance, the recipient's reduction lowers the tax on their earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 40 + employment_income: 15_000 + dividend_income: 5_000 + transferor: + age: 38 + employment_income: 10_000 + benunits: + benunit: + members: [recipient, transferor] + households: + household: + members: [recipient, transferor] + output: + # The transferor (10,000, below the cut allowance of 11,310) elects, and + # the recipient's tax falls by 20% x 1,260 = 252. Tax on the recipient's + # earnings: (15,000 - 12,570) x 20% = 486, less the 252 reduction = 234. Dividends: (5,000 - 500) x 10.75% = 483.75, + # not deducted. + marriage_allowance: [1_260, 0] + income_tax: [717.75, 0] + uc_income_tax_on_earnings: [234, 0] + # (15,000 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: [194.40, 0] + uc_individual_earned_income: [14_571.60, 10_000] + uc_earned_income: 24_571.60 + +- name: Voluntary Class 3 NI is not deducted + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 13_000 + ni_class_3: 900 + output: + national_insurance: 934.40 + uc_national_insurance_on_earnings: 34.40 + # 13,000 - 86 - 34.40. + uc_earned_income: 12_879.60 + +- name: Pension contributions come off only the contributor's own earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 20_000 + employee_pension_contributions: 1_000 + would_claim_marriage_allowance: false + partner: + age: 38 + personal_pension_contributions: 1_000 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [earner, partner] + households: + household: + members: [earner, partner] + output: + # Relief on the earner's contributions: (20,000 - 12,570 - 1,000) x 20%. + uc_income_tax_on_earnings: [1_286, 0] + # 20,000 - 1,000 - 1,286 - 594.40. The partner has no earnings, so their + # own contributions reduce nothing. + uc_individual_earned_income: [17_119.60, 0] + uc_earned_income: 17_119.60 + +- name: High Income Child Benefit Charge is not deducted + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 70_000 + child: + age: 3 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + # Child Benefit 27.05 x 52 = 1,406.60; the charge takes + # (70,000 - 60,000) / 200 = 50% of it, 703.30. + CB_HITC: [703.30, 0] + income_tax: [16_135.30, 0] + # 37,700 x 20% + (57,430 - 37,700) x 40%. + uc_income_tax_on_earnings: [15_432, 0] + +- name: From 2027 property income has its own rates, and its tax is still not deducted + period: 2027 + absolute_error_margin: 0.01 + input: + age: 35 + employment_income: 20_000 + property_income: 5_000 + output: + # Property: (5,000 - 1,000) x 22% = 880. + property_income_tax: 880 + income_tax: 2_366 + uc_income_tax_on_earnings: 1_486 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml new file mode 100644 index 000000000..c88a124cb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml @@ -0,0 +1,187 @@ +# State Pension is retirement pension income, which counts in full as +# Universal Credit unearned income: UC Regs 2013 reg. 66(1)(a), with the +# meaning in reg. 67(1) taken from State Pension Credit Act 2002 s. 16(1)(za) +# (new State Pension) and (a) (Category A or B retirement pension). Only a +# mixed-age couple can have both a UC award and a State Pension, because UC +# needs a member under State Pension age. +# +# Hand calculations use the 2026-27 rates read in April 2026: +# - couple standard allowance (one over 25): 666.97 a month = 8,003.64 a year; +# - earned income reduction rate: 55%; +# - council rent of 6,240 a year is met in full by the housing costs element. +# So the maximum amount is 8,003.64 + 6,240 = 14,243.64. + +- name: State Pension counts as unearned income + period: 2026 + absolute_error_margin: 0.01 + input: + state_pension: 9_000 + output: + uc_unearned_income: 9_000 + +- name: State Pension adds to private pension income + period: 2026 + absolute_error_margin: 0.01 + input: + state_pension: 9_000 + private_pension_income: 1_000 + output: + uc_unearned_income: 10_000 + +- name: Reported new State Pension flows through to unearned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension_type: NEW + # 9,000 / 52 = 173.08 a week, under the full new State Pension, so + # all of it is new_state_pension and none is a protected payment. + state_pension_reported: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + output: + state_pension: [9_000, 0] + uc_unearned_income: 9_000 + +- name: Mixed-age couple renting from the council, State Pension reduces UC pound for pound + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + council_tax: 1_500 + output: + uc_standard_allowance: 8_003.64 + uc_housing_costs_element: 6_240 + uc_maximum_amount: 14_243.64 + uc_unearned_income: 9_000 + uc_income_reduction: 9_000 + # 14,243.64 - 9,000. Before this fix the award was 14,243.64. + universal_credit: 5_243.64 + # The pension-age CTR income is State Pension plus UC: + # 9,000 + 5,243.64 = 14,243.64, below the applicable amount of + # 383.35 x 52 = 19,934.20, so council tax is met in full. Before this fix + # the inflated UC gave an income of 23,243.64 and CTR of + # 1,500 - 20% x (23,243.64 - 19,934.20) = 838.11. + council_tax_benefit: 1_500 + +- name: Same couple with the same amount of private pension instead gets the same UC + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + council_tax: 1_500 + output: + uc_unearned_income: 9_000 + universal_credit: 5_243.64 + +- name: State Pension above the maximum amount removes UC entirely + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 20_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + uc_unearned_income: 20_000 + # The reduction is capped at the maximum amount. + uc_income_reduction: 14_243.64 + universal_credit: 0 + +- name: State Pension and the younger partner's earnings both reduce UC + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + # Below the personal allowance and the NI primary threshold, so net + # earnings equal gross earnings. + employment_income: 6_000 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + uc_earned_income: 6_000 + # No children and no limited capability for work, so no work allowance: + # 55% x 6,000 = 3,300 of earnings plus 9,000 of State Pension. + uc_income_reduction: 12_300 + # 14,243.64 - 12,300. + universal_credit: 1_943.64 + +- name: State Pension also counts in 2020-21 + period: 2020 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + # Couple standard allowance 596.58 a month (with the 2020 uplift): + # 7,158.96 + 6,240 = 13,398.96; less 9,000 of State Pension. + uc_maximum_amount: 13_398.96 + uc_unearned_income: 9_000 + universal_credit: 4_398.96 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml index 521f62d95..cd7102739 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml @@ -62,3 +62,545 @@ output: marriage_allowance: [0, 0, 0, 0] income_tax: [9_432, 4_486, 0, 0] + +# ITA 2007 ss. 55A-55E. 2026-27: personal allowance 12,570 (s. 35(1)), so the +# transferable amount is 10% x 12,570 = 1,257, rounded up to 1,260 +# (s. 55B(4)(b), (5)). The transferor's allowance falls to 11,310 (s. 55B(6)); +# the recipient's tax falls by 20% x 1,260 = 252 at Step 6 (s. 55A(2), +# s. 55B(1), (3)), capped at their Step 5 tax (s. 29(2)). The couple elects +# only when that lowers their combined tax. + +- name: Issue example - transferor between the cut and the full allowance + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + employment_income: 12_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Transferor: (12,000 - 11,310) x 20% = 138. Recipient: + # (30,000 - 12,570) x 20% - 252 = 3,486 - 252 = 3,234. The couple pays + # 3,372 against 3,486 without the election. + makes_marriage_allowance_election: [false, true] + marriage_allowance_relinquished: [0, 1_260] + marriage_allowance: [1_260, 0] + personal_allowance: [12_570, 11_310] + marriage_allowance_tax_reduction: [252, 0] + income_tax: [3_234, 138] + +- name: Transferor with no income + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + makes_marriage_allowance_election: [false, true] + marriage_allowance: [1_260, 0] + income_tax: [3_234, 0] + +- name: Transferor just under the allowance still saves the couple tax + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + employment_income: 12_500 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Transferor: (12,500 - 11,310) x 20% = 238, less than the 252 saved. + makes_marriage_allowance_election: [false, true] + income_tax: [3_234, 238] + +- name: Transferor using the whole allowance gains nothing, so the couple does not elect + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + employment_income: 12_570 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Electing would cost the transferor 1,260 x 20% = 252, exactly the + # recipient's reduction. + makes_marriage_allowance_election: [false, false] + marriage_allowance: [0, 0] + personal_allowance: [12_570, 12_570] + income_tax: [3_486, 0] + +- name: Issue departure 1 - transferor with 15,000 of interest stays at the starting rate for savings + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + savings_interest_income: 15_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Transferor: 15,000 - 11,310 = 3,690 of interest, all inside the 5,000 + # starting rate for savings at 0% (s. 12), so they can elect + # (s. 55C(1)(c)) and pay nothing more. Recipient: 3,486 - 252 = 3,234. + makes_marriage_allowance_election: [false, true] + personal_allowance: [12_570, 11_310] + income_tax: [3_234, 0] + +- name: Issue departure 1 - transferor with 12,000 of interest + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + savings_interest_income: 12_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + makes_marriage_allowance_election: [false, true] + income_tax: [3_234, 0] + +- name: Issue departure 2 - recipient taxed mostly on dividends keeps the reduction + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 12_570 + dividend_income: 5_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Earnings use the whole allowance. Dividends: 500 at the dividend nil + # rate, 4,500 x 10.75% = 483.75 at Step 5, less 252 = 231.75. + marriage_allowance: [1_260, 0] + marriage_allowance_tax_reduction: [252, 0] + income_tax: [231.75, 0] + +- name: Issue departure 3 - Scottish intermediate-rate recipient saves the Scottish basic rate + period: 2026 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + employment_income: 5_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + households: + household: + members: [recipient, transferor] + region: SCOTLAND + output: + # Recipient: 17,430 taxable = 3,967 x 19% + 12,989 x 20% + 474 x 21% + # = 753.73 + 2,597.80 + 99.54 = 3,451.07, less 20% x 1,260 = 252, not + # the 21% marginal rate. + marriage_allowance_appropriate_percentage: [0.2, 0.2] + marriage_allowance_tax_reduction: [252, 0] + income_tax: [3_199.07, 0] + +- name: Scottish starter-rate transferor + period: 2026 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + employment_income: 12_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + households: + household: + members: [recipient, transferor] + region: SCOTLAND + output: + # Transferor: 690 x 19% = 131.10, less than the 252 saved. + makes_marriage_allowance_election: [false, true] + income_tax: [3_199.07, 131.10] + +- name: Issue departure 5 - both partners below the allowance, no election and no double counting + period: 2026 + input: + people: + p1: + age: 40 + employment_income: 5_000 + p2: + age: 38 + employment_income: 6_000 + benunits: + benunit: + members: [p1, p2] + is_married: true + output: + # Neither has tax to reduce. + makes_marriage_allowance_election: [false, false] + marriage_allowance: [0, 0] + income_tax: [0, 0] + +- name: Both partners basic-rate taxpayers, no election + period: 2026 + input: + people: + p1: + age: 40 + employment_income: 30_000 + p2: + age: 38 + employment_income: 20_000 + benunits: + benunit: + members: [p1, p2] + is_married: true + output: + makes_marriage_allowance_election: [false, false] + income_tax: [3_486, 1_486] + +- name: Higher-rate recipient cannot gain (s. 55B(2)(b)) + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 60_000 + transferor: + age: 38 + employment_income: 5_000 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # 37,700 x 20% + 9,730 x 40% = 11,432. + makes_marriage_allowance_election: [false, false] + marriage_allowance: [0, 0] + income_tax: [11_432, 0] + +- name: Issue departure 6 - no election when married couple's allowance is claimed (s. 55B(2)(d)) + period: 2026 + input: + people: + # Married couple's allowance needs a spouse born before 6 April 1935. + recipient: + age: 92 + employment_income: 30_000 + married_couples_allowance: 11_270 + transferor: + age: 90 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # 3,486 less 10% x 11,270 = 1,127. + makes_marriage_allowance_election: [false, false] + marriage_allowance: [0, 0] + income_tax: [2_359, 0] + +- name: The couple's take-up follows the spouse who would gain + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + would_claim_marriage_allowance: false + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + makes_marriage_allowance_election: [false, false] + income_tax: [3_486, 0] + +- name: The electing spouse's take-up value does not stop the election + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + makes_marriage_allowance_election: [false, true] + income_tax: [3_234, 0] + +- name: The lower earner elects whichever spouse is older + period: 2026 + input: + people: + transferor: + age: 40 + employment_income: 8_000 + recipient: + age: 38 + employment_income: 30_000 + benunits: + benunit: + members: [transferor, recipient] + is_married: true + output: + makes_marriage_allowance_election: [true, false] + marriage_allowance: [0, 1_260] + personal_allowance: [11_310, 12_570] + income_tax: [0, 3_234] + +- name: Reduction capped at the recipient's Step 5 tax (s. 29(2)) + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 13_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Step 5 tax (13,000 - 12,570) x 20% = 86, so the reduction is 86. + makes_marriage_allowance_election: [false, true] + marriage_allowance: [1_260, 0] + marriage_allowance_tax_reduction: [86, 0] + income_tax: [0, 0] + +- name: Other tax reductions leave less tax for the reduction (s. 27(2), s. 29(2)-(3)) + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 15_000 + other_tax_credits: 300 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Step 5 tax 2,430 x 20% = 486, less 300 leaves 186 for the reduction. + marriage_allowance_tax_reduction_limit: [186, 0] + marriage_allowance_tax_reduction: [186, 0] + income_tax: [0, 0] + +- name: When either spouse's election saves the same, the elder elects + period: 2026 + input: + people: + p1: + age: 40 + savings_interest_income: 15_000 + dividend_income: 10_000 + p2: + age: 38 + savings_interest_income: 15_000 + dividend_income: 10_000 + benunits: + benunit: + members: [p1, p2] + is_married: true + output: + # The spouses are identical, so either election saves the same and the + # elder elects. Income tax is not asserted: the model sets allowances + # against savings before dividends, where s. 25(2) requires the order + # that most reduces tax. + makes_marriage_allowance_election: [true, false] + personal_allowance: [11_310, 12_570] + +- name: Children in the benefit unit are not spouses + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 30_000 + transferor: + age: 38 + child: + age: 5 + benunits: + benunit: + members: [recipient, transferor, child] + is_married: true + output: + is_marriage_allowance_spouse: [true, true, false] + makes_marriage_allowance_election: [false, true, false] + marriage_allowance: [1_260, 0, 0] + income_tax: [3_234, 0, 0] + +- name: s. 55B(2)(ba) - dividends in the nil rate above the basic rate limit bar the recipient + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 50_070 + dividend_income: 500 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # Taxable 37,500 + 500: without the dividend nil rate 300 of dividends + # would be at the dividend upper rate. + makes_marriage_allowance_election: [false, false] + income_tax: [7_500, 0] + +- name: Savings in the savings nil rate above the basic rate limit do not bar the recipient + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 50_070 + savings_interest_income: 500 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # The interest is at the savings nil rate, which s. 55B(2)(b) allows: + # 37,500 x 20% - 252 = 7,248. + makes_marriage_allowance_election: [false, true] + income_tax: [7_248, 0] + +- name: Scottish intermediate-rate recipient with savings banded on UK rates can gain + period: 2026 + input: + people: + recipient: + age: 40 + employment_income: 40_000 + savings_interest_income: 5_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + households: + household: + members: [recipient, transferor] + region: SCOTLAND + output: + makes_marriage_allowance_election: [false, true] + marriage_allowance_tax_reduction: [252, 0] + +- name: Pensioner transferor with State Pension, as in the UC State Pension test + period: 2026 + input: + people: + pensioner: + age: 70 + state_pension: 12_000 + partner: + age: 45 + employment_income: 20_000 + benunits: + benunit: + members: [pensioner, partner] + is_married: true + output: + # Pensioner: 690 x 20% = 138. Partner: 7,430 x 20% - 252 = 1,234. + makes_marriage_allowance_election: [true, false] + personal_allowance: [11_310, 12_570] + income_tax: [138, 1_234] + +- name: 2015-16 transferable amount is 1,060 (s. 55B(4)(a)) + period: 2015 + input: + people: + recipient: + age: 40 + employment_income: 20_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # (20,000 - 10,600) x 20% = 1,880, less 20% x 1,060 = 212. + marriage_allowance_transferable_amount: [1_060, 1_060] + personal_allowance: [10_600, 9_540] + income_tax: [1_668, 0] + +- name: 2019-20 transferable amount is 10% of 12,500 + period: 2019 + input: + people: + recipient: + age: 40 + employment_income: 20_000 + transferor: + age: 38 + benunits: + benunit: + members: [recipient, transferor] + is_married: true + output: + # 7,500 x 20% = 1,500, less 20% x 1,250 = 250. + marriage_allowance_transferable_amount: [1_250, 1_250] + income_tax: [1_250, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml index 064d9679f..3549367fd 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml @@ -1,41 +1,151 @@ -- name: Basic rate band is eligible for Marriage Allowance - period: 2021 +# ITA 2007 s. 55B(2)(b), (ba) and s. 55C(1)(c), (ca): only the basic rates, +# the Scottish starter and intermediate rates, the dividend ordinary rate and +# the nil and starting rates for savings. 2026-27: personal allowance 12,570, +# basic rate limit 37,700, Scottish higher rate from 31,092 of taxable income. + +- name: Basic-rate earner meets the conditions + period: 2026 + input: + employment_income: 30_000 + output: + meets_marriage_allowance_income_conditions: true + +- name: No income meets the conditions + period: 2026 input: - tax_band: BASIC + employment_income: 0 output: meets_marriage_allowance_income_conditions: true -- name: Higher rate band is ineligible for Marriage Allowance - period: 2021 +- name: Taxable income exactly at the basic rate limit meets the conditions + period: 2026 input: - tax_band: HIGHER + # 50,270 - 12,570 = 37,700, all at the basic rate. + employment_income: 50_270 + output: + meets_marriage_allowance_income_conditions: true + +- name: One pound into the higher rate fails the conditions + period: 2026 + input: + employment_income: 50_271 output: meets_marriage_allowance_income_conditions: false -- name: Additional rate band is ineligible for Marriage Allowance - period: 2021 +- name: Additional-rate earner fails the conditions + period: 2026 input: - tax_band: ADDITIONAL + employment_income: 150_000 output: meets_marriage_allowance_income_conditions: false -- name: Below-PA earner (band NONE) is eligible for Marriage Allowance (regression for #395) - period: 2021 +- name: Transferor test uses the cut allowance (s. 55C(1)(c)) + period: 2026 input: - tax_band: NONE + # Allowance 12,570 - 1,260 = 11,310, so 50,000 leaves 38,690 taxable, + # 990 above the basic rate limit. + employment_income: 50_000 + marriage_allowance_relinquished: 1_260 + output: + personal_allowance: 11_310 + meets_marriage_allowance_income_conditions: false + +- name: The same income meets the conditions with the full allowance + period: 2026 + input: + # 50,000 - 12,570 = 37,430, within the basic rate band. + employment_income: 50_000 + output: + meets_marriage_allowance_income_conditions: true + +- name: Scottish intermediate-rate earner meets the conditions + period: 2026 + input: + # 40,000 - 12,570 = 27,430, below the Scottish higher rate at 31,092. + employment_income: 40_000 + region: SCOTLAND output: meets_marriage_allowance_income_conditions: true -- name: Scottish starter rate band is eligible for Marriage Allowance - period: 2021 +- name: Scottish starter-rate earner meets the conditions + period: 2026 input: - tax_band: STARTER + employment_income: 14_000 + region: SCOTLAND output: meets_marriage_allowance_income_conditions: true -- name: Scottish intermediate rate band is eligible for Marriage Allowance - period: 2021 +- name: Scottish higher-rate earner fails the conditions + period: 2026 + input: + # 44,000 - 12,570 = 31,430, above 31,092. + employment_income: 44_000 + region: SCOTLAND + output: + meets_marriage_allowance_income_conditions: false + +- name: Scottish savings are banded on UK rates, so an intermediate-rate earner with savings meets the conditions + period: 2026 input: - tax_band: INTERMEDIATE + # Non-savings 27,430 is below the Scottish higher rate; savings stack on + # it to 32,430, below the UK basic rate limit of 37,700. + employment_income: 40_000 + savings_interest_income: 5_000 + region: SCOTLAND output: meets_marriage_allowance_income_conditions: true + +- name: Savings within the savings nil rate above the basic rate limit meet the conditions + period: 2026 + input: + # Non-savings 50,070 - 12,570 = 37,500. The 500 of interest is covered by + # the savings nil rate (s. 12A), which s. 55B(2)(b) allows, so none of it + # is charged at the savings higher rate. + employment_income: 50_070 + savings_interest_income: 500 + output: + meets_marriage_allowance_income_conditions: true + +- name: Dividends within the dividend nil rate above the basic rate limit fail the conditions + period: 2026 + input: + # The same 500 as dividends: s. 55B(2)(ba) ignores the dividend nil rate + # (s. 13A), and 37,500 + 500 = 38,000 would put 300 at the dividend upper + # rate. + employment_income: 50_070 + dividend_income: 500 + output: + meets_marriage_allowance_income_conditions: false + +- name: Savings charged at the savings higher rate fail the conditions + period: 2026 + input: + # Taxable income 37,500 + 2,000 = 39,500. A higher-rate taxpayer's + # savings nil rate covers 500, leaving savings at the higher rate. + employment_income: 50_070 + savings_interest_income: 2_000 + output: + meets_marriage_allowance_income_conditions: false + +- name: The dividend test uses the basic rate limit (2025-26, 37,700) + period: 2025 + input: + # Taxable income 49,970 - 12,570 = 37,400, plus 250 of dividends within + # the dividend allowance: 37,650 is inside the basic rate limit, so no + # dividends would be at the dividend upper rate even without s. 13A. + employment_income: 49_970 + dividend_income: 250 + output: + meets_marriage_allowance_income_conditions: true + +- name: Dividends above property income cross the basic rate limit and fail the conditions + period: 2026 + input: + # Property 47,000 less the 1,000 property allowance and the allowance of + # 12,570 leaves 33,430 of non-savings income. Dividends sit on top + # (s. 16), from 33,430 to 38,430, so 730 would be at the dividend upper + # rate (s. 55B(2)(ba)). + property_income: 47_000 + dividend_income: 5_000 + output: + meets_marriage_allowance_income_conditions: false diff --git a/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml b/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml index 0b70c7eeb..0b5b40ab7 100644 --- a/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml +++ b/policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml @@ -51,9 +51,10 @@ household: members: [high_earner, low_earner] output: - # Low earner's unused PA (12,570 − 10,000 = 2,570) is capped at PA × ma_rate - # = 12,570 × 0.1 = 1,257, then rounded up to the nearest £10 → 1,260. - # Low earner's partner has no unused PA to transfer, so low_earner's MA is 0. + # The low earner elects: the transferable amount is PA x ma_rate = + # 12,570 x 0.1 = 1,257, rounded up to the nearest £10 → 1,260, and costs + # them nothing (10,000 is below 11,310). With the income condition lifted, + # the higher-rate earner gains it; the low earner receives nothing. marriage_allowance: [1260, 0] - name: Expanded MA — explicit Marriage Allowance take-up input drives the reform @@ -152,3 +153,30 @@ members: [high_earner, low_earner] output: marriage_allowance: [0, 0] + +- name: Expanded MA — rate-only expansion keeps the higher-rate bar + period: 2026 + absolute_error_margin: 1 + reforms: + - policyengine_uk.reforms.cps.marriage_tax_reforms.expanded_ma_reform_rate_only + input: + people: + high_earner: + age: 35 + employment_income: 60_000 + low_earner: + age: 33 + employment_income: 8_000 + benunits: + benunit: + members: [high_earner, low_earner] + is_married: true + households: + household: + members: [high_earner, low_earner] + output: + # Without remove_income_condition the higher-rate earner still cannot + # gain (ITA 2007 s. 55B(2)(b)): 37,700 x 20% + 9,730 x 40% = 11,432. + makes_marriage_allowance_election: [false, false] + marriage_allowance: [0, 0] + income_tax: [11_432, 0] diff --git a/policyengine_uk/tests/test_marriage_allowance_properties.py b/policyengine_uk/tests/test_marriage_allowance_properties.py new file mode 100644 index 000000000..d0927e90d --- /dev/null +++ b/policyengine_uk/tests/test_marriage_allowance_properties.py @@ -0,0 +1,351 @@ +"""Property-based tests for the Marriage Allowance (ITA 2007 ss. 55A-55E). + +An election by one spouse or civil partner cuts their own personal allowance +by the transferable amount (s. 55B(6)) and gives the other a Step 6 tax +reduction of the appropriate percentage of it, capped at their Step 5 tax +(s. 55A(2), s. 55B(1), (3), s. 29(2)). The couple elects only when the +conditions allow it and it lowers their combined income tax. + +Invariants, for any generated population of couples with every kind of +taxable income, in England, Wales or Scotland: + +1. The couple's income tax is never higher than with no election. +2. Differential against brute force: the model's election gives the lowest + couple income tax of no election, the first spouse electing and the second + spouse electing, among those the conditions allow, each run with the + election fixed. +3. Once a spouse has elected, the gaining partner's reduction, allowance and + income tax do not depend on the electing spouse's income, and the electing + spouse's allowance falls by exactly the transferable amount. +4. Accounting: in each couple what one spouse gives up the other receives, at + most one spouse elects, the gaining partner's tax falls by exactly the + reduction, and the reduction is at most the appropriate percentage of the + transferable amount and the tax left to reduce. +5. The couple's income tax, with the election the model makes, never falls + when either spouse has more income. +6. No one outside a married couple gives up or receives anything. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2019 has a 1,250 transferable amount, 2026 current rates and the 10.75% +# dividend ordinary rate, 2027 the property and savings rates (Finance Act +# 2026). +YEARS = [2019, 2026, 2027] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +INCOMES = { + "employment_income": 60_000, + "self_employment_income": 20_000, + "private_pension_income": 20_000, + "state_pension": 15_000, + "savings_interest_income": 20_000, + "dividend_income": 15_000, + "property_income": 10_000, +} +ELECTION = "makes_marriage_allowance_election" +VARIABLES = [ + ELECTION, + "is_marriage_allowance_spouse", + "marriage_allowance_transferable_amount", + "marriage_allowance_relinquished", + "marriage_allowance", + "marriage_allowance_appropriate_percentage", + "marriage_allowance_tax_reduction", + "marriage_allowance_tax_reduction_limit", + "meets_marriage_allowance_income_conditions", + "personal_allowance", + "income_tax", +] +TOLERANCE = 0.01 + + +def amounts(cap): + # Many incomes are zero, so the allowances and bands are often unused. + return st.one_of(st.just(0.0), st.floats(0, cap)) + + +@st.composite +def adults(draw): + age = draw(st.integers(18, 100)) + adult = dict(age=age) + for variable, cap in INCOMES.items(): + if variable == "state_pension" and age < 67: + continue + adult[variable] = draw(amounts(cap)) + return adult + + +@st.composite +def families(draw): + return dict( + adults=[draw(adults()) for _ in range(draw(st.sampled_from([1, 2, 2, 2])))], + married=draw(st.sampled_from([True, True, True, False])), + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + region=draw(st.sampled_from(REGIONS)), + # s. 55B(2)(d): now and then the first adult claims married couple's + # allowance. + married_couples_allowance=draw(st.sampled_from([0.0] * 9 + [11_270.0])), + ) + + +def situation(units, year, election=None, bump=None): + """One simulation holding every family. + + ``election``, one value per person in order, fixes who elects. + ``bump`` is (family index, adult index, variable, amount) to add. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + person = {"state_pension": {year: 0.0}} + for variable, value in adult.items(): + person[variable] = {year: value} + if j == 0: + person["married_couples_allowance"] = { + year: unit["married_couples_allowance"] + } + if bump is not None and bump[:2] == (i, j): + person[bump[2]] = {year: adult.get(bump[2], 0.0) + bump[3]} + people[f"p{i}_{j}"] = person + names.append(f"p{i}_{j}") + for k, age in enumerate(unit["children"]): + people[f"c{i}_{k}"] = {"age": {year: age}} + names.append(f"c{i}_{k}") + benunits[f"b{i}"] = { + "members": names, + "is_married": {year: unit["married"] and len(unit["adults"]) == 2}, + } + households[f"h{i}"] = {"members": names, "region": {year: unit["region"]}} + if election is not None: + for person, elects in zip(people.values(), election): + person[ELECTION] = {year: bool(elects)} + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs)) + values = {v: np.asarray(sim.calculate(v, year)) for v in VARIABLES} + values["benunit"] = np.asarray(sim.populations["benunit"].members_entity_id) + return values + + +def per_person(units, adult_value, child_value=0.0): + """One value per person in input order: adults, then children.""" + return np.array( + [ + value + for unit in units + for value in [adult_value(unit, j) for j in range(len(unit["adults"]))] + + [child_value] * len(unit["children"]) + ] + ) + + +def couple_total(values, variable): + """Each person's couple total of ``variable``.""" + total = np.bincount(values["benunit"], weights=values[variable]) + return total[values["benunit"]] + + +def spouses_by_order(values): + """Masks for the first and second spouse of each couple, in input order.""" + spouse = values["is_marriage_allowance_spouse"] + first = np.zeros_like(spouse) + second = np.zeros_like(spouse) + seen = set() + for i, (is_spouse, unit) in enumerate(zip(spouse, values["benunit"])): + if not is_spouse: + continue + if unit in seen: + second[i] = True + else: + first[i] = True + seen.add(unit) + return first, second + + +couple_populations = st.lists(families(), min_size=1, max_size=8) + + +@PROPERTY_SETTINGS +@given(units=couple_populations, year=st.sampled_from(YEARS)) +def test_election_never_raises_the_couples_tax(units, year): + model = calculate(units, year) + none = calculate(units, year, election=np.zeros_like(model[ELECTION])) + model_tax = couple_total(model, "income_tax") + none_tax = couple_total(none, "income_tax") + assert np.all(model_tax <= none_tax + TOLERANCE), units + # A couple that elects saves at least a penny. + elects = couple_total(model, ELECTION) > 0 + assert np.all(model_tax[elects] <= none_tax[elects] - TOLERANCE + 1e-6), units + + +@PROPERTY_SETTINGS +@given(units=couple_populations, year=st.sampled_from(YEARS)) +def test_election_is_the_best_allowed_choice(units, year): + model = calculate(units, year) + none = calculate(units, year, election=np.zeros_like(model[ELECTION])) + first, second = spouses_by_order(model) + options = [none] + allowed = [np.ones_like(first)] + claims_mca = per_person( + units, lambda unit, j: float(j == 0 and unit["married_couples_allowance"] > 0) + ) + no_mca = couple_total(dict(benunit=model["benunit"], mca=claims_mca), "mca") == 0 + for elector in [first, second]: + run = calculate(units, year, election=elector) + gainer = model["is_marriage_allowance_spouse"] & ~elector + # s. 55C(1)(b), (c): the elector has an allowance and, with it cut, + # pays only basic rates; s. 55B(2)(b): so does the gaining partner; + # s. 55B(2)(d): no married couple's allowance. + conditions = ( + elector + & (none["personal_allowance"] > 0) + & run["meets_marriage_allowance_income_conditions"] + ) | (gainer & none["meets_marriage_allowance_income_conditions"]) + couple_ok = ( + couple_total(dict(benunit=model["benunit"], ok=conditions), "ok") == 2 + ) & no_mca + options.append(run) + allowed.append(couple_ok) + best = couple_total(none, "income_tax") + for run, ok in zip(options[1:], allowed[1:]): + best = np.where(ok, np.minimum(best, couple_total(run, "income_tax")), best) + np.testing.assert_allclose( + couple_total(model, "income_tax"), best, atol=TOLERANCE, err_msg=str(units) + ) + + +@st.composite +def bumped(draw): + units = draw(st.lists(families(), min_size=1, max_size=8)) + i = draw(st.integers(0, len(units) - 1)) + j = draw(st.integers(0, len(units[i]["adults"]) - 1)) + variables = [ + v for v in INCOMES if v != "state_pension" or units[i]["adults"][j]["age"] >= 67 + ] + return units, (i, j, draw(st.sampled_from(variables)), draw(st.floats(1, 30_000))) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_gaining_partner_is_independent_of_the_electors_income(case, year): + units, (i, j, variable, amount) = case + base = calculate(units, year) + # The bumped adult elects wherever they are a spouse. + person_index = ( + sum(len(unit["adults"]) + len(unit["children"]) for unit in units[:i]) + j + ) + election = np.zeros_like(base[ELECTION]) + election[person_index] = base["is_marriage_allowance_spouse"][person_index] + low = calculate(units, year, election=election) + high = calculate(units, year, election=election, bump=(i, j, variable, amount)) + others = np.arange(len(election)) != person_index + for variable_name in [ + "marriage_allowance", + "marriage_allowance_tax_reduction", + "personal_allowance", + "income_tax", + ]: + np.testing.assert_allclose( + high[variable_name][others], + low[variable_name][others], + atol=TOLERANCE, + err_msg=f"{variable_name} {case}", + ) + # s. 55B(6): the elector's allowance falls by the transferable amount. + none = calculate(units, year, election=np.zeros_like(election)) + for run in [low]: + np.testing.assert_allclose( + run["personal_allowance"], + np.maximum( + 0, + none["personal_allowance"] + - election * run["marriage_allowance_transferable_amount"], + ), + atol=TOLERANCE, + err_msg=str(case), + ) + + +@PROPERTY_SETTINGS +@given(units=couple_populations, year=st.sampled_from(YEARS)) +def test_marriage_allowance_accounting(units, year): + model = calculate(units, year) + none = calculate(units, year, election=np.zeros_like(model[ELECTION])) + spouse = model["is_marriage_allowance_spouse"] + elects = model[ELECTION] + # Only spouses elect, at most one per couple. + assert not np.any(elects & ~spouse), units + assert np.all(couple_total(model, ELECTION) <= 1), units + # What one spouse gives up the other receives. + np.testing.assert_allclose( + couple_total(model, "marriage_allowance"), + couple_total(model, "marriage_allowance_relinquished"), + atol=TOLERANCE, + ) + transferable = model["marriage_allowance_transferable_amount"] + received = model["marriage_allowance"] + assert np.all(np.isclose(received, 0) | np.isclose(received, transferable)), units + # The reduction is bounded by s. 55B(1), (3) and s. 29(2). + reduction = model["marriage_allowance_tax_reduction"] + rate = model["marriage_allowance_appropriate_percentage"] + assert np.all(reduction <= rate * received + TOLERANCE), units + assert np.all( + reduction <= model["marriage_allowance_tax_reduction_limit"] + TOLERANCE + ), units + # The gaining partner's tax falls by exactly the reduction. + gainer = received > 0 + np.testing.assert_allclose( + model["income_tax"][gainer], + none["income_tax"][gainer] - reduction[gainer], + atol=TOLERANCE, + err_msg=str(units), + ) + # The elector pays at least as much as without the election. + assert np.all( + model["income_tax"][elects] >= none["income_tax"][elects] - TOLERANCE + ), units + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_couples_tax_never_falls_with_more_income(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump) + assert np.all( + couple_total(high, "income_tax") >= couple_total(low, "income_tax") - TOLERANCE + ), (bump, units) + + +@PROPERTY_SETTINGS +@given(units=couple_populations, year=st.sampled_from(YEARS)) +def test_only_married_couples_transfer(units, year): + model = calculate(units, year) + outside = ~model["is_marriage_allowance_spouse"] + for variable in [ + ELECTION, + "marriage_allowance", + "marriage_allowance_relinquished", + "marriage_allowance_tax_reduction", + ]: + assert np.all(model[variable][outside] == 0), (variable, units) + married_pairs = { + i + for i, unit in enumerate(units) + if unit["married"] and len(unit["adults"]) == 2 + } + spouse_units = set(model["benunit"][model["is_marriage_allowance_spouse"]]) + assert spouse_units == married_pairs, units diff --git a/policyengine_uk/tests/test_uc_earnings_deductions_properties.py b/policyengine_uk/tests/test_uc_earnings_deductions_properties.py new file mode 100644 index 000000000..e6a2954b5 --- /dev/null +++ b/policyengine_uk/tests/test_uc_earnings_deductions_properties.py @@ -0,0 +1,270 @@ +"""Property-based tests for the Universal Credit deductions from earnings. + +UC Regs 2013 reg. 55(5) and reg. 57(2) deduct from a person's earnings only +their own relievable pension contributions and the income tax and National +Insurance they pay in respect of their employment or trade. The model takes +earnings as the lowest slice of the person's non-savings income, after their +allowances, with other income above it (ITA 2007 s. 16 for savings and +dividends). + +Invariants, for any generated population of single people and couples with +earnings, self-employment and every kind of taxable unearned income: + +1. Unearned income never changes earned income: giving any adult more State + Pension, private pension, property, savings or dividend income leaves + every person's uc_individual_earned_income, and so uc_earned_income, + unchanged. +2. Monotone: the UC award, before and after the benefit cap, is + non-increasing in each kind of unearned income for each adult. +3. Differential against the tax engine: the income tax deducted equals the + income tax the same person pays when their earnings are their only + income, and the NI deducted equals their NI. +4. Differential against the formula this replaced: earned income is never + lower, and UC never higher, than when the whole benefit unit's tax was + deducted, because only deductions were removed. + +Invariants 1-3 hold only while unearned income leaves the person's +allowances alone, so the generated incomes keep adjusted net income below +the personal allowance taper (100,000) and no one is old enough for the +married couple's allowance. Marriage Allowance is claimed. Each couple's +election is held at its choice in the first run: the gaining partner's +reduction then comes off their tax on earnings whatever anyone's unearned +income, but the couple's choice to elect responds to it (see +test_uc_state_pension_properties.py). Invariant 4 compares runs that elect +alike, since the tax is the same in both. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import * +from policyengine_uk.utils.scenario import Scenario + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2020 has the temporary UC uplift; 2026 current rates; 2027 the new property +# and savings rates (Finance Act 2026). +YEARS = [2020, 2026, 2027] +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", +] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +UNEARNED = [ + "state_pension", + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", +] +# Married couple's allowance needs a birth before 6 April 1935 (85 in 2020). +PENSION_AGE = st.integers(67, 84) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single": [WORKING_AGE], + "couple": [WORKING_AGE, WORKING_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], +} +# Per adult: earnings up to 50,000 and unearned income up to 5 x 7,000, +# plus a bump up to 9,000, keeps adjusted net income under 100,000. +earnings = st.one_of(st.just(0.0), st.floats(0, 30_000)) +self_employment = st.one_of(st.just(0.0), st.floats(0, 20_000)) +unearned = st.one_of(st.just(0.0), st.floats(0, 7_000)) +bumps = st.floats(1, 9_000) +UC_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "uc_earned_income", +] +ELECTION = "makes_marriage_allowance_election" +PERSON_VARIABLES = [ + ELECTION, + "uc_individual_earned_income", + "uc_income_tax_on_earnings", + "uc_national_insurance_on_earnings", + "income_tax", + "national_insurance", +] + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(list(SHAPES))) + adults = [] + for age in [draw(age) for age in SHAPES[shape]]: + adult = dict( + age=age, + employment_income=draw(earnings), + self_employment_income=draw(self_employment), + # Some self-employed are in a start-up period, so the minimum + # income floor does not apply. + uc_is_in_startup_period=draw(st.booleans()), + ) + for variable in UNEARNED: + if variable == "state_pension" and age < 67: + continue + adult[variable] = draw(unearned) + adults.append(adult) + return dict( + adults=adults, + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 15_000)), + region=draw(st.sampled_from(REGIONS)), + ) + + +def situation(units, year, bump=None, earnings_only=False, election=None): + """One simulation holding every family. + + ``bump`` is (family index, adult index, variable, amount) to add. + ``earnings_only`` drops every kind of unearned income. ``election``, one + value per person in order, fixes who makes a Marriage Allowance election. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"p{i}_{j}" + person = {"state_pension": {year: 0.0}} + for variable, value in adult.items(): + if earnings_only and variable in UNEARNED: + continue + person[variable] = {year: value} + if bump is not None and bump[:2] == (i, j) and not earnings_only: + variable, amount = bump[2], bump[3] + person[variable] = {year: adult.get(variable, 0.0) + amount} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + # Marriage Allowance needs a marriage or civil partnership + # (ITA 2007 s. 55C(1)(a)). + "is_married": {year: len(unit["adults"]) == 2}, + } + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "region": {year: unit["region"]}, + } + if election is not None: + for person, elects in zip(people.values(), election): + person[ELECTION] = {year: bool(elects)} + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, scenario=None, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs), scenario=scenario) + values = {v: np.asarray(sim.calculate(v, year)) for v in UC_VARIABLES} + for v in PERSON_VARIABLES: + values[v] = np.asarray(sim.calculate(v, year)) + values["is_adult_person"] = np.asarray(sim.calculate("age", year)) >= 18 + return values + + +@st.composite +def bumped(draw): + units = draw(st.lists(families(), min_size=1, max_size=10)) + i = draw(st.integers(0, len(units) - 1)) + j = draw(st.integers(0, len(units[i]["adults"]) - 1)) + variables = UNEARNED if units[i]["adults"][j]["age"] >= 67 else UNEARNED[1:] + return units, (i, j, draw(st.sampled_from(variables)), draw(bumps)) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_unearned_income_never_changes_earned_income(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump, election=low[ELECTION]) + for variable in ["uc_individual_earned_income", "uc_earned_income"]: + np.testing.assert_allclose( + high[variable], low[variable], atol=0.01, err_msg=f"{bump} {units}" + ) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_uc_is_non_increasing_in_unearned_income(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump, election=low[ELECTION]) + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(high[variable] <= low[variable] + 0.01), (variable, bump, units) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=10), + year=st.sampled_from(YEARS), +) +def test_deductions_equal_tax_and_ni_on_earnings_alone(units, year): + full = calculate(units, year) + alone = calculate(units, year, earnings_only=True, election=full[ELECTION]) + adult = full["is_adult_person"] + np.testing.assert_allclose( + full["uc_income_tax_on_earnings"][adult], + alone["income_tax"][adult], + atol=0.01, + err_msg=str(units), + ) + np.testing.assert_allclose( + full["uc_national_insurance_on_earnings"][adult], + alone["national_insurance"][adult], + atol=0.01, + err_msg=str(units), + ) + # Never more than the person's own tax and NI. + assert np.all(full["uc_income_tax_on_earnings"] <= full["income_tax"] + 0.01) + assert np.all( + full["uc_national_insurance_on_earnings"] <= full["national_insurance"] + 0.01 + ) + + +def _use_formula_before_1942(simulation): + class uc_earned_income(Variable): + value_type = float + entity = BenUnit + label = "UC earned income as computed before #1942" + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + gross = add(benunit, period, ["uc_mif_capped_earned_income"]) + disregards = add( + benunit, + period, + ["uc_work_allowance", "benunit_tax", "pension_contributions"], + ) + return max_(0, gross - disregards) + + simulation.tax_benefit_system.update_variable(uc_earned_income) + + +BEFORE_1942 = Scenario(simulation_modifier=_use_formula_before_1942) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=10), + year=st.sampled_from(YEARS), +) +def test_fix_only_removes_deductions(units, year): + after = calculate(units, year) + before = calculate(units, year, scenario=BEFORE_1942) + assert np.all(after["uc_earned_income"] >= before["uc_earned_income"] - 0.01), units + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(after[variable] <= before[variable] + 0.01), (variable, units) diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py new file mode 100644 index 000000000..f7bedc074 --- /dev/null +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -0,0 +1,320 @@ +"""Property-based tests for State Pension in the Universal Credit means test. + +UC Regs 2013 reg. 66(1)(a) counts retirement pension income as unearned +income, and reg. 67(1) gives that phrase its State Pension Credit Act 2002 +s. 16 meaning, which covers State Pension (s. 16(1)(za) and (a)) and private +pensions (s. 16(1)(f)) alike. Reg. 22(1)(a) deducts all unearned income from +the maximum amount. Only a mixed-age couple can hold both a UC award and a +State Pension, because UC needs a member under State Pension age. + +Invariants, for any generated population of families: + +1. Monotone: the UC award, before and after the benefit cap, is + non-increasing in State Pension, and more State Pension adds exactly that + much to unearned income without changing the maximum amount. +2. Pound for pound: raising State Pension by d lowers the award before the + benefit cap by exactly min(d, award), whatever the partner earns, because + tax on State Pension never comes off earnings (reg. 55(5)(b), reg. 57(2) + step 3). +3. Equivalence: UC with State Pension x equals UC with the same x of private + pension income received by the same person instead (both are retirement + pension income, taxed the same way). It also equals UC with x of property + income (held without property capital, so it is not treated as capital + yield under reg. 72), although property income is taxed on less. + +Marriage Allowance is claimed throughout. Once a couple has elected, the +gaining partner's reduction is fixed (ITA 2007 s. 55B(1), (4)-(6)), so the +pensioner's State Pension cannot move the earning partner's tax on earnings. +The couple's election itself responds to the pensioner's income: the couple +stops electing once it no longer lowers their tax (from 12,570 of State +Pension in 2026-27), which raises the earner's tax and so the award. So +invariants 1-3 hold each couple's election at its choice in the first run; +test_couple_stops_electing_when_it_no_longer_saves_tax pins the change in +election (PolicyEngine/policyengine-uk#1947). +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2020 has the temporary standard allowance uplift, 2026 the current rates. +YEARS = [2020, 2026] +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", + "OWNED_WITH_MORTGAGE", +] +# State Pension age is 66 until 2026-27 finishes phasing up; 67 and over is +# unambiguously pension age and 60 and under unambiguously working age. +PENSION_AGE = st.integers(67, 100) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "mixed_age": [PENSION_AGE, WORKING_AGE], + "single_pension": [PENSION_AGE], + "couple_pension": [PENSION_AGE, PENSION_AGE], +} +money = st.floats(0, 30_000, allow_nan=False, allow_infinity=False) +UC_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "uc_earned_income", + "uc_unearned_income", + "uc_income_reduction", + "uc_maximum_amount", +] +ELECTION = "makes_marriage_allowance_election" + + +@st.composite +def families(draw): + # Weight towards mixed-age couples, the only shape UC can reach. + shape = draw( + st.sampled_from(["mixed_age", "mixed_age", "single_pension", "couple_pension"]) + ) + earnings = draw(st.one_of(st.just(0.0), money)) + return dict( + ages=[draw(age) for age in SHAPES[shape]], + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + earnings=earnings, + state_pension=draw(money), + ) + + +def situation( + units, + year, + income_variable="state_pension", + pension_bump=0.0, + election=None, +): + """Build one simulation holding every family. + + The eldest adult receives the family's State Pension (plus + ``pension_bump``) under ``income_variable``; when that is not + ``state_pension``, their State Pension is set to zero so the same amount + arrives as the other income instead. A working-age partner receives the + family's earnings. ``election``, one value per person in order, fixes + who makes a Marriage Allowance election. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, age in enumerate(unit["ages"]): + name = f"p{i}_{j}" + person = {"age": {year: age}, "state_pension": {year: 0.0}} + if j == 0: + amount = unit["state_pension"] + pension_bump + person[income_variable] = {year: amount} + elif age < 67: + person["employment_income"] = {year: unit["earnings"]} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + # Marriage Allowance needs a marriage or civil partnership + # (ITA 2007 s. 55C(1)(a)). + "is_married": {year: len(unit["ages"]) == 2}, + } + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "savings": {year: unit["savings"]}, + } + if election is not None: + for person, elects in zip(people.values(), election): + person[ELECTION] = {year: bool(elects)} + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs)) + return {v: np.asarray(sim.calculate(v, year)) for v in UC_VARIABLES + [ELECTION]} + + +def assert_same(a, b, message): + for variable in UC_VARIABLES: + np.testing.assert_allclose( + a[variable], b[variable], atol=0.01, err_msg=f"{variable}: {message}" + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), + year=st.sampled_from(YEARS), +) +def test_uc_is_non_increasing_in_state_pension(units, bump, year): + low = calculate(units, year) + high = calculate(units, year, pension_bump=bump, election=low[ELECTION]) + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(high[variable] <= low[variable] + 0.01), (variable, units) + # State Pension changes nothing in the maximum amount, and all of it is + # unearned income. + np.testing.assert_allclose( + high["uc_maximum_amount"], low["uc_maximum_amount"], atol=0.01 + ) + np.testing.assert_allclose( + high["uc_unearned_income"] - low["uc_unearned_income"], + bump, + atol=0.01, + err_msg=str(units), + ) + # Families with no working-age adult never get UC. + no_working_age_adult = np.array([min(unit["ages"]) >= 67 for unit in units]) + assert np.all(high["universal_credit"][no_working_age_adult] == 0), units + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), + year=st.sampled_from(YEARS), +) +def test_uc_falls_pound_for_pound_in_state_pension(units, bump, year): + # Earnings in the family change nothing: tax on State Pension is never + # deducted from anyone's earnings (reg. 55(5)(b), reg. 57(2) step 3), and + # an election already made fixes the earner's Marriage Allowance. + low = calculate(units, year) + high = calculate(units, year, pension_bump=bump, election=low[ELECTION]) + award = low["universal_credit_pre_benefit_cap"] + np.testing.assert_allclose( + high["universal_credit_pre_benefit_cap"], + award - np.minimum(bump, award), + atol=0.01, + err_msg=str(units), + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + year=st.sampled_from(YEARS), +) +def test_state_pension_counts_like_private_pension(units, year): + assert_same( + calculate(units, year), + calculate(units, year, income_variable="private_pension_income"), + str(units), + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + year=st.sampled_from(YEARS), +) +def test_state_pension_counts_like_property_income(units, year): + # Property income is taxed on less (the 1,000 property allowance), but + # neither tax comes off the partner's earnings. The lower tax can change + # whether the couple elects, so hold the election at the State Pension + # run's choice. + pension = calculate(units, year) + assert_same( + pension, + calculate( + units, + year, + income_variable="property_income", + election=pension[ELECTION], + ), + str(units), + ) + + +def test_tax_on_state_pension_does_not_reduce_partners_earned_income(): + # 2026: pensioner aged 70 with State Pension 16,000 pays income tax of + # (16,000 - 12,570) x 20% = 686. The partner aged 45 earns 13,000 and pays + # income tax of 86 and NI of 34.40. Either spouse electing for Marriage + # Allowance would cost them 252 and save the other 252, so the couple + # does not elect. Council rent 20,000. + # Earned income = 13,000 - 86 - 34.40 = 12,879.60 (reg. 55(5)(b)). + # Maximum amount = 12 x 666.97 + 20,000 = 28,003.64. + # UC = 28,003.64 - (0.55 x 12,879.60 + 16,000) = 4,919.86. + unit = dict( + ages=[70, 45], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=20_000.0, + savings=0.0, + earnings=13_000.0, + state_pension=16_000.0, + ) + values = calculate([unit], 2026) + assert values["uc_earned_income"][0] == pytest.approx(12_879.60, abs=0.01) + assert values["universal_credit"][0] == pytest.approx(4_919.86, abs=0.01) + + +def test_state_pension_does_not_change_partners_marriage_allowance(): + # 2026: pensioner aged 70, partner aged 45 earning 20,000, council rent + # 20,000. Once the pensioner elects, ITA 2007 s. 55B gives the partner a + # fixed reduction of 20% x 1,260 = 252 and cuts the pensioner's own + # allowance by 1,260 (s. 55B(6)); the pensioner's State Pension of 12,000 + # keeps them within the basic rate, so they can elect (s. 55C(1)(c)). + # Partner's tax on earnings = (20,000 - 12,570) x 20% - 252 = 1,234 and + # NI (20,000 - 12,569.96) x 8% = 594.40, so earned income = 18,171.60 + # at any State Pension while the election saves the couple tax (below + # 12,570). + # UC = 8,003.64 + 20,000 - (0.55 x 18,171.60 + 12,000) = 6,009.26. + unit = dict( + ages=[70, 45], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=20_000.0, + savings=0.0, + earnings=20_000.0, + state_pension=12_000.0, + ) + values = calculate([unit], 2026) + assert values["uc_earned_income"][0] == pytest.approx(18_171.60, abs=0.01) + assert values["universal_credit"][0] == pytest.approx(6_009.26, abs=0.01) + + +def test_couple_stops_electing_when_it_no_longer_saves_tax(): + # 2026, the couple above with State Pension of 12,500 and 12,600. At + # 12,500 electing costs the pensioner (12,500 - 11,310) x 20% = 238 and + # saves the partner 252, so the couple elects and the partner's earned + # income is 18,171.60 as above: + # UC = 28,003.64 - (0.55 x 18,171.60 + 12,500) = 5,509.26. + # At 12,600 it would cost 252, the same as it saves, so the couple does not + # elect. The partner's tax on earnings is 1,486, earned income + # 20,000 - 1,486 - 594.40 = 17,919.60, and + # UC = 28,003.64 - (0.55 x 17,919.60 + 12,600) = 5,547.86. + # The election weighs only the couple's income tax, not their UC, so 100 + # more State Pension raises UC by 38.60 here. + unit = dict( + ages=[70, 45], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=20_000.0, + savings=0.0, + earnings=20_000.0, + state_pension=12_500.0, + ) + low = calculate([unit], 2026) + high = calculate([unit], 2026, pension_bump=100.0) + assert low[ELECTION].tolist() == [True, False] + assert high[ELECTION].tolist() == [False, False] + assert low["uc_earned_income"][0] == pytest.approx(18_171.60, abs=0.01) + assert high["uc_earned_income"][0] == pytest.approx(17_919.60, abs=0.01) + assert low["universal_credit"][0] == pytest.approx(5_509.26, abs=0.01) + assert high["universal_credit"][0] == pytest.approx(5_547.86, abs=0.01) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py index 8ff40e4a8..1880148d3 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py @@ -4,17 +4,18 @@ class uc_earned_income(Variable): value_type = float entity = BenUnit - label = "Universal Credit earned income (after disregards and tax)" + label = "Universal Credit earned income (after deductions and work allowance)" definition_period = YEAR unit = GBP + reference = dict( + title="Universal Credit Regulations 2013 reg. 22(1)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", + ) def formula(benunit, period, parameters): - personal_gross_earned_income = add( - benunit, period, ["uc_mif_capped_earned_income"] - ) - disregards = add( - benunit, - period, - ["uc_work_allowance", "benunit_tax", "pension_contributions"], - ) - return max_(0, personal_gross_earned_income - disregards) + # Each person's earned income is net of their own deductions + # (reg. 55(5), reg. 57(2)); the work allowance then comes off the + # combined earned income before the taper (reg. 22(1)(b)). + earned_income = add(benunit, period, ["uc_individual_earned_income"]) + work_allowance = benunit("uc_work_allowance", period) + return max_(0, earned_income - work_allowance) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py new file mode 100644 index 000000000..bade3e5c5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py @@ -0,0 +1,88 @@ +from policyengine_uk.model_api import * + + +class uc_income_tax_on_earnings(Variable): + value_type = float + entity = Person + label = "Universal Credit deduction for income tax on the person's earnings" + documentation = ( + "Income tax the person pays in respect of their own employment and " + "self-employment, which Universal Credit deducts from their earned " + "income. Tax on pensions, State Pension, property, savings and " + "dividends is not deducted, nor is the High Income Child Benefit " + "Charge or the pension annual allowance charge." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2), step 3", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + dict( + title="Income Tax Act 2007 s. 16", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/16", + ), + ] + + def formula(person, period, parameters): + # Reg. 55(5)(b) deducts income tax paid by the person "in respect of + # the employment" and reg. 57(2) step 3 income tax paid "in respect + # of any trade". Neither says how to split a person's tax when they + # also have other income. Earnings are taken as the lowest slice of + # their non-savings income, after the allowances they actually have: + # savings and dividends sit above all non-savings income (as in ITA + # 2007 s. 16), property income above the rest of it (as in s. 16A + # from 2027-28), and other non-savings income (private pensions, + # State Pension, taxable benefits) above earnings. So tax on other + # income never comes off earnings. Under RTI, DWP deducts the PAYE + # actually taken on the job, which can include tax on a State + # Pension coded against it; that is not modelled. + p = parameters(period) + earnings_components = [ + "taxable_employment_income", + "taxable_self_employment_income", + "taxable_miscellaneous_income", + ] + # Match the gross earnings in uc_mif_capped_earned_income. + bi = p.gov.contrib.ubi_center.basic_income.interactions + if bi.include_in_means_tests and bi.include_in_taxable_income: + earnings_components.append("basic_income") + earnings = add(person, period, earnings_components) + # earned_taxable_income is non-savings, non-property income after + # allowances. The part of it above earnings belongs to the person's + # other non-savings income. + non_savings_non_property_income = person("adjusted_net_income", period) - add( + person, + period, + [ + "taxable_savings_interest_income", + "taxable_dividend_income", + "taxable_property_income", + ], + ) + other_income = max_(0, non_savings_non_property_income - earnings) + taxable_earnings = max_( + 0, person("earned_taxable_income", period) - other_income + ) + rates = p.gov.hmrc.income_tax.rates + tax = where( + person("pays_scottish_income_tax", period), + rates.scotland.rates.calc(taxable_earnings), + rates.uk.calc(taxable_earnings), + ) + # HMRC gives a Marriage Allowance recipient the transfer through + # their tax code (code letter M), so it comes off the tax on their + # earnings first. + tax = max_(0, tax - person("marriage_allowance_tax_reduction", period)) + # Other tax reductions (for example the married couple's allowance) + # can leave total income tax below the tax on the earnings slice; + # never deduct more than the person pays. That sets them against tax + # on other income first, the opposite order to the Marriage + # Allowance; the married couple's allowance needs a birth before + # 6 April 1935, so it hardly ever reaches a UC claimant. + return min_(tax, person("income_tax", period)) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py new file mode 100644 index 000000000..7995416bc --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class uc_individual_earned_income(Variable): + value_type = float + entity = Person + label = "Universal Credit earned income of the person" + documentation = ( + "The person's earned income for Universal Credit, after the " + "deductions for their own relievable pension contributions and their " + "own income tax and National Insurance in respect of their employment " + "and self-employment, before the work allowance." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + ] + + 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. + gross_earnings = person("uc_mif_capped_earned_income", period) + deductions = add( + person, + period, + [ + "pension_contributions", + "uc_income_tax_on_earnings", + "uc_national_insurance_on_earnings", + ], + ) + return max_(0, gross_earnings - deductions) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py new file mode 100644 index 000000000..93ca6e8d7 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class uc_national_insurance_on_earnings(Variable): + value_type = float + entity = Person + label = "Universal Credit deduction for National Insurance on the person's earnings" + documentation = ( + "Primary Class 1 contributions on the person's employment and Class 2 " + "and Class 4 contributions on their trade, which Universal Credit " + "deducts from their earned income. Voluntary Class 3 contributions " + "are not in respect of an employment or trade, so are not deducted." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2), step 3", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + ] + adds = [ + "ni_class_1_employee", + "ni_class_2", + "ni_class_4", + ] diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/is_marriage_allowance_spouse.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/is_marriage_allowance_spouse.py new file mode 100644 index 000000000..9a1c567a3 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/is_marriage_allowance_spouse.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * + + +class is_marriage_allowance_spouse(Variable): + value_type = bool + entity = Person + label = "Spouse or civil partner for Marriage Allowance" + documentation = ( + "Whether this person is one of the married couple or civil partners " + "in their benefit unit, who can make or gain from a Marriage " + "Allowance election. These are the two eldest members whose marital " + "status is married, and only when the benefit unit has two. The Act " + "needs only a marriage or civil partnership, not living together, but " + "a spouse outside the benefit unit is not modelled." + ) + definition_period = YEAR + reference = dict( + title="Income Tax Act 2007 s. 55C(1)(a)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55C", + ) + + def formula(person, period, parameters): + status = person("marital_status", period) + minimum_age = parameters( + period + ).gov.hmrc.income_tax.allowances.marriage_allowance.minimum_spouse_age + married = (status == status.possible_values.MARRIED) & ( + person("age", period) >= minimum_age + ) + # Without a marital status input, every member of a married benefit + # unit, children included, is married; the two eldest old enough to + # marry are the couple. + rank = person.get_rank( + person.benunit, -person("age", period), condition=married + ) + spouse = married & (rank < 2) + return spouse & (person.benunit.sum(spouse) == 2) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/makes_marriage_allowance_election.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/makes_marriage_allowance_election.py new file mode 100644 index 000000000..a13da510a --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/makes_marriage_allowance_election.py @@ -0,0 +1,166 @@ +from policyengine_uk.model_api import * + +BRANCH_OUTPUTS = [ + "income_tax", + "personal_allowance", + "marriage_allowance_tax_reduction_limit", + "meets_marriage_allowance_income_conditions", +] +# Inputs to income tax that do not depend on the election. Resolving them +# before branching lets both branches reuse them, and keeps the result +# independent of which variable a caller asks for first (a reform that +# branches inside adjusted_net_income would otherwise see a cached election). +INDEPENDENT_OF_ELECTION = [ + "age", + "adjusted_net_income", + "gift_aid_grossed_up", + "gift_aid", + "blind_persons_allowance", + "covenanted_payments", + "charitable_investment_gifts", + "other_deductions", + "pension_contributions_relief", + "taxable_savings_interest_income", + "taxable_dividend_income", + "taxable_property_income", + "pays_scottish_income_tax", + "CB_HITC", + "other_tax_credits", + "married_couples_allowance", +] + + +def tax_position(simulation, name, period, relinquished): + """Each person's tax with no election in force and the given allowance cut. + + The branch holds the election off, so no one gains a reduction, and cuts + each person's personal allowance by ``relinquished`` (ITA 2007 s. 55B(6)). + The branch is dropped afterwards, and its copied arrays freed, so a later + period starts from a fresh copy of the simulation. + """ + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + branch.set_input( + "makes_marriage_allowance_election", + period, + np.zeros(len(relinquished), dtype=bool), + ) + branch.set_input("marriage_allowance_relinquished", period, relinquished) + population = branch.populations["person"] + return {variable: population(variable, period) for variable in BRANCH_OUTPUTS} + finally: + del simulation.branches[name] + for population in branch.populations.values(): + for holder in population._holders.values(): + holder._memory_storage._arrays.clear() + + +class makes_marriage_allowance_election(Variable): + value_type = bool + entity = Person + label = "Makes a Marriage Allowance election" + documentation = ( + "Whether this person elects to give up part of their personal " + "allowance so that their spouse or civil partner gets the Marriage " + "Allowance tax reduction. The couple elects only when the election " + "is allowed and lowers their combined income tax, in whichever " + "direction lowers it more, and only if the spouse who would gain has " + "would_claim_marriage_allowance true. Set as an input, an election " + "is taken to be one the other spouse is entitled to gain from: the " + "conditions are checked only when the model makes the choice." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007 s. 55B", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Income Tax Act 2007 s. 55C", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55C", + ), + dict( + title="Income Tax Act 2007 s. 55E", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55E", + ), + ] + + def formula(person, period, parameters): + for variable in INDEPENDENT_OF_ELECTION: + person(variable, period) + spouse = person("is_marriage_allowance_spouse", period) + transferable = person("marriage_allowance_transferable_amount", period) + simulation = person.simulation + without = tax_position( + simulation, + "marriage_allowance_without_election", + period, + np.zeros_like(transferable), + ) + relinquishing = tax_position( + simulation, + "marriage_allowance_relinquishing", + period, + spouse * transferable, + ) + + def partner(values): + # The other spouse's value, for each spouse. + return person.benunit.sum(spouse * values) - spouse * values + + # If this person elects, the other spouse's tax falls by the + # appropriate percentage of this person's transferable amount, capped + # at the tax they have left (s. 55B(1), (3); s. 29(2)), and this + # person pays tax on the allowance they give up. + rate = person("marriage_allowance_appropriate_percentage", period) + partner_reduction = min_( + partner(rate) * transferable, + partner(without["marriage_allowance_tax_reduction_limit"]), + ) + extra_tax = relinquishing["income_tax"] - without["income_tax"] + # Tax is computed to the penny; smaller differences are rounding. + saving = np.round(partner_reduction - extra_tax, 2) + married_couples_allowance_claimed = person.benunit.any( + spouse & (person("married_couples_allowance", period) > 0) + ) + allowed = ( + spouse + # s. 55B(2)(d): neither spouse claims married couple's allowance. + & ~married_couples_allowance_claimed + # s. 55C(1)(b): the electing spouse is entitled to an allowance. + & (without["personal_allowance"] > 0) + # s. 55C(1)(c), (ca): only basic rates once the allowance is cut. + & relinquishing["meets_marriage_allowance_income_conditions"] + # s. 55B(2)(b), (ba): the gaining party pays only basic rates. + & (partner(without["meets_marriage_allowance_income_conditions"]) > 0) + ) + # Elect only to lower the couple's tax. Only one spouse elects. s. 55E + # allows each of them one election and one reduction and does not + # bar elections both ways, which would lower some couples' tax + # further (where giving up allowance costs less than the basic rate, + # as on dividends or at the Scottish starter rate), but HMRC's + # guidance describes a single transfer from the lower earner. + saving_if_allowed = where(allowed & (saving > 0), saving, 0) + other_saving = partner(saving_if_allowed) + # On a tie the spouse with the lower adjusted net income elects, then + # the elder, then the first listed. + income = person("adjusted_net_income", period) + other_income = partner(income) + elder = ( + person.get_rank(person.benunit, -person("age", period), condition=spouse) + == 0 + ) + first_on_tie = (income < other_income) | ((income == other_income) & elder) + chooses = (saving_if_allowed > 0) & ( + (saving_if_allowed > other_saving) + | ((saving_if_allowed == other_saving) & first_on_tie) + ) + # The couple's take-up draw is the gaining spouse's, the same draw + # that decided take-up when the transfer was modelled on the + # recipient alone. + partner_would_claim = ( + partner(person("would_claim_marriage_allowance", period)) > 0 + ) + return chooses & partner_would_claim diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py index a848ad993..ec3ee2b6d 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance.py @@ -4,8 +4,11 @@ class would_claim_marriage_allowance(Variable): label = "Would claim Marriage Allowance" documentation = ( - "Whether this person would claim Marriage Allowance if eligible. " - "Generated stochastically in the dataset using take-up rates." + "Whether this person would claim Marriage Allowance from their spouse " + "or civil partner's election when it lowers the couple's income tax. " + "Generated stochastically in the dataset using take-up rates. The " + "couple's election follows the value on the spouse who would gain " + "(makes_marriage_allowance_election)." ) entity = Person definition_period = YEAR @@ -19,31 +22,31 @@ class would_claim_marriage_allowance(Variable): class marriage_allowance(Variable): value_type = float entity = Person - label = "Marriage Allowance" + label = "Marriage Allowance received" + documentation = ( + "The transferable amount this person's spouse or civil partner gives " + "up under a Marriage Allowance election. This person's tax falls by " + "the appropriate percentage of it (marriage_allowance_tax_reduction); " + "the allowance itself does not come off their taxable income." + ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/ukpga/2007/3/part/3/chapter/3A" + reference = [ + dict( + title="Income Tax Act 2007 s. 55B", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Income Tax Act 2007 s. 55C", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55C", + ), + ] unit = GBP def formula(person, period, parameters): - marital = person("marital_status", period) - married = marital == marital.possible_values.MARRIED - is_adult = person("is_adult", period) - eligible = ( - married - & is_adult - & 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 - 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 + # The gaining party is the other spouse or civil partner of the person + # who elects (s. 55C(1)(a)); at most one of the couple elects. + spouse = person("is_marriage_allowance_spouse", period) + given_up = person("makes_marriage_allowance_election", period) * person( + "marriage_allowance_transferable_amount", period ) - # Use pre-generated take-up decision from dataset - would_claim = person("would_claim_marriage_allowance", period) - return eligible * amount_if_eligible * would_claim + return spouse * (person.benunit.sum(spouse * given_up) - given_up) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py new file mode 100644 index 000000000..1554e2652 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_appropriate_percentage.py @@ -0,0 +1,31 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_appropriate_percentage(Variable): + value_type = float + entity = Person + label = "Marriage Allowance appropriate percentage" + documentation = ( + "The rate at which the Marriage Allowance reduces the gaining party's " + "tax: the basic rate, or the Scottish basic rate for a Scottish " + "taxpayer, whatever rate their income is actually taxed at. The Welsh " + "basic rate equals the basic rate in this model." + ) + definition_period = YEAR + reference = dict( + title="Income Tax Act 2007 s. 55B(3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ) + unit = "/1" + + def formula(person, period, parameters): + rates = parameters(period).gov.hmrc.income_tax.rates + # The Scottish basic rate follows the starter rate; before 2018-19 the + # Scottish scale had no starter rate. + scottish = rates.scotland.rates.rates + scottish_basic = scottish[1 if len(scottish) > 3 else 0] + return where( + person("pays_scottish_income_tax", period), + scottish_basic, + rates.uk.rates[0], + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_relinquished.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_relinquished.py new file mode 100644 index 000000000..48fb598df --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_relinquished.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_relinquished(Variable): + value_type = float + entity = Person + label = "Personal allowance given up under a Marriage Allowance election" + documentation = ( + "The transferable amount by which a Marriage Allowance election cuts " + "the electing person's own personal allowance." + ) + definition_period = YEAR + reference = dict( + title="Income Tax Act 2007 s. 55B(6)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ) + unit = GBP + + def formula(person, period, parameters): + # Only a spouse or civil partner can give allowance up. + elects = person("makes_marriage_allowance_election", period) & person( + "is_marriage_allowance_spouse", period + ) + return elects * person("marriage_allowance_transferable_amount", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py new file mode 100644 index 000000000..826fcfc7a --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction.py @@ -0,0 +1,34 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_tax_reduction(Variable): + value_type = float + entity = Person + label = "Marriage Allowance tax reduction" + documentation = ( + "The gaining party's tax reduction at Step 6 of the income tax " + "calculation: the appropriate percentage of the transferable amount, " + "capped at the tax left to reduce." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007 s. 55A(2)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55A", + ), + dict( + title="Income Tax Act 2007 s. 55B(1) and (3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Income Tax Act 2007 s. 29(2)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/29", + ), + ] + unit = GBP + + def formula(person, period, parameters): + reduction = person("marriage_allowance_appropriate_percentage", period) * ( + person("marriage_allowance", period) + ) + return min_(reduction, person("marriage_allowance_tax_reduction_limit", period)) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction_limit.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction_limit.py new file mode 100644 index 000000000..db38ac65d --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_tax_reduction_limit.py @@ -0,0 +1,32 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_tax_reduction_limit(Variable): + value_type = float + entity = Person + label = "Tax available for the Marriage Allowance reduction" + documentation = ( + "Tax at Step 5 of the income tax calculation left after the person's " + "other tax reductions. The Marriage Allowance tax reduction cannot " + "exceed it. Deducting the other reductions first gives the same total " + "as the order that most reduces the person's liability. Double " + "taxation relief should come after the Marriage Allowance (s. 27(6)), " + "but other_tax_credits does not separate it out." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax Act 2007 s. 29(2) and (3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/29", + ), + dict( + title="Income Tax Act 2007 s. 27(2)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/27", + ), + ] + unit = GBP + + def formula(person, period, parameters): + step_5_tax = person("income_tax_pre_charges", period) + other_reductions = add(person, period, ["capped_mcad", "other_tax_credits"]) + return max_(0, step_5_tax - other_reductions) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_transferable_amount.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_transferable_amount.py new file mode 100644 index 000000000..a26dbec28 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/marriage_allowance_transferable_amount.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class marriage_allowance_transferable_amount(Variable): + value_type = float + entity = Person + label = "Marriage Allowance transferable amount" + documentation = ( + "The fixed amount of personal allowance a Marriage Allowance election " + "transfers: 10% of the personal allowance in ITA 2007 s. 35(1), " + "rounded up to a multiple of £10. It does not depend on how much of " + "the transferor's allowance is unused." + ) + definition_period = YEAR + reference = dict( + title="Income Tax Act 2007 s. 55B(4) and (5)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ) + unit = GBP + + def formula(person, period, parameters): + allowances = parameters(period).gov.hmrc.income_tax.allowances + ma = allowances.marriage_allowance + amount = allowances.personal_allowance.amount * ma.max + increment = ma.rounding_increment + return np.ceil(amount / increment) * increment diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py index e91f14cbd..00a89c6c5 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.py @@ -1,18 +1,84 @@ from policyengine_uk.model_api import * -import numpy as np + + +def liable_only_at_marriage_allowance_rates(person, period, parameters): + """Whether the person pays no income tax above the basic rates. + + ITA 2007 s. 55B(2)(b) and s. 55C(1)(c) allow only the basic rate, the + property, default, savings, Scottish and Welsh basic rates, the Scottish + starter and intermediate rates, the dividend ordinary rate and the nil and + starting rates for savings. So no non-savings income may reach the higher + rate (the Scottish higher rate for a Scottish taxpayer), and no savings the + savings higher rate. Dividends covered by the dividend nil rate still + count: s. 55B(2)(ba) and s. 55C(1)(ca) test the dividend upper rate as if + s. 13A were omitted. + """ + rates = parameters(period).gov.hmrc.income_tax.rates + savings = person("taxable_savings_interest_income", period) + dividends = person("taxable_dividend_income", period) + # Earnings, pensions, property and other non-savings income after + # allowances, as in property_income_tax. + non_savings = max_( + 0, + person("adjusted_net_income", period) + - savings + - dividends + - person("allowances", period), + ) + # The Scottish higher rate follows the starter, basic and intermediate + # rates; before 2018-19 the Scottish scale had only the basic rate below it. + scottish = rates.scotland.rates + scottish_higher = scottish.thresholds[3 if len(scottish.rates) > 3 else 1] + non_savings_limit = where( + person("pays_scottish_income_tax", period), + scottish_higher, + rates.uk.thresholds[1], + ) + savings_above_basic = add( + person, period, ["higher_rate_savings_income", "add_rate_savings_income"] + ) + # Dividends are the top slice, above all non-savings income (property + # included) and savings (s. 16), counted before the dividend allowance; + # the dividend upper rate starts at the basic rate limit (s. 13). The + # savings test above uses the model's own savings bands. + below_dividends = non_savings + max_( + 0, savings - person("received_allowances_savings_income", period) + ) + dividends_after_allowances = max_( + 0, dividends - person("received_allowances_dividend_income", period) + ) + dividends_above_basic = (dividends_after_allowances > 0) & ( + below_dividends + dividends_after_allowances > rates.uk.thresholds[1] + ) + return ( + (non_savings <= non_savings_limit) + & (savings_above_basic <= 0) + & ~dividends_above_basic + ) 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, given their actual allowances. The " + "gaining party must meet this (ITA 2007 s. 55B(2)(b), (ba)); the " + "electing spouse must meet it with their allowance already cut by " + "the transferable amount (s. 55C(1)(c), (ca))." + ) entity = Person definition_period = YEAR value_type = bool - reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/55B" + reference = [ + dict( + title="Income Tax Act 2007 s. 55B(2)(b) and (ba)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + dict( + title="Income Tax Act 2007 s. 55C(1)(c) and (ca)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55C", + ), + ] def formula(person, period, parameters): - band = person("tax_band", period) - eligible_bands = parameters( - period - ).gov.hmrc.income_tax.allowances.marriage_allowance.eligible_bands - return np.isin(band.decode_to_str(), eligible_bands) + return liable_only_at_marriage_allowance_rates(person, period, parameters) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py deleted file mode 100644 index 8d26ee5cb..000000000 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/partners_unused_personal_allowance.py +++ /dev/null @@ -1,21 +0,0 @@ -from policyengine_uk.model_api import * -from numpy import ceil - - -class partners_unused_personal_allowance(Variable): - label = "Partner's unused personal allowance" - documentation = ( - "The personal tax allowance not used by this person's partner, if they exist" - ) - entity = Person - definition_period = YEAR - value_type = float - unit = GBP - - def formula(person, period, parameters): - is_adult = person("is_adult", period) - pa = person("unused_personal_allowance", period) - # Subtract this person's own unused PA only if they are an adult, so - # non-adults (whose PA isn't part of the adult-summed pool) cannot - # produce a negative transferable amount. - return person.benunit.sum(is_adult * pa) - is_adult * pa diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py index 52efd2fab..19b60ca94 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/personal_allowance.py @@ -7,10 +7,16 @@ class personal_allowance(Variable): label = "Personal Allowance for the year" unit = GBP definition_period = YEAR - reference = dict( - title="Income Tax Act 2007 s. 35, s. 58", - href="https://www.legislation.gov.uk/ukpga/2007/3/section/35", - ) + reference = [ + dict( + title="Income Tax Act 2007 s. 35, s. 58", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/35", + ), + dict( + title="Income Tax Act 2007 s. 55B(6)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/55B", + ), + ] def formula(person, period, parameters): params = parameters(period) @@ -23,4 +29,8 @@ def formula(person, period, parameters): ANI_for_taper = ANI - gift_aid_grossed_up excess = max_(0, ANI_for_taper - PA.maximum_ANI) reduction = excess * PA.reduction_rate - return max_(0, np.ceil(personal_allowance - reduction)) + allowance = max_(0, np.ceil(personal_allowance - reduction)) + # A Marriage Allowance election cuts the electing spouse's allowance + # by the transferable amount (s. 55B(6)). + relinquished = person("marriage_allowance_relinquished", period) + return max_(0, allowance - relinquished)