diff --git a/changelog.d/2326.fixed.md b/changelog.d/2326.fixed.md new file mode 100644 index 0000000000..d486a2a5a0 --- /dev/null +++ b/changelog.d/2326.fixed.md @@ -0,0 +1 @@ +- Stop relieving a miscellaneous loss against other income in Income Tax, adjusted net income, total income and Universal Credit earnings: ITA 2007 s. 152(4) allows it only against miscellaneous income. diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.yaml new file mode 100644 index 0000000000..08d5fcf444 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.yaml @@ -0,0 +1,63 @@ +# A miscellaneous loss is relieved only against miscellaneous income +# (ITA 2007 s. 152(4)). 2026-27: personal allowance £12,570, basic rate 20% +# to £50,270, higher rate 40%. + +- name: Case 1, miscellaneous income is taxed with other income. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 30_000 + miscellaneous_income: 5_000 + output: + taxable_miscellaneous_income: 5_000 + # (35,000 - 12,570) x 20%. + income_tax: 4_486 + +- name: Case 2, a miscellaneous loss does not reduce tax on pay. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 50_000 + miscellaneous_income: -10_000 + output: + taxable_miscellaneous_income: 0 + # (50,000 - 12,570) x 20%, as with no loss. + income_tax: 7_486 + +- name: Case 3, nor adjusted net income. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 110_000 + miscellaneous_income: -20_000 + output: + taxable_miscellaneous_income: 0 + adjusted_net_income: 110_000 + # Personal allowance 12,570 - (110,000 - 100,000) / 2 = 7,570; taxable + # 102,430: 37,700 x 20% + 64,730 x 40% = 33,432. + income_tax: 33_432 + +- name: Case 4, total income ignores a miscellaneous loss. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + private_pension_income: 40_000 + miscellaneous_income: -10_000 + output: + total_income: 40_000 + +- name: Case 5, a miscellaneous loss does not reduce Universal Credit earnings. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + employment_income: 30_000 + miscellaneous_income: -10_000 + output: + # 30,000 less income tax (30,000 - 12,570) x 20% = 3,486 and primary + # Class 1 (30,000 - 12,570) x 8% = 1,394.40. + uc_individual_earned_income_before_mif: 30_000 - 3_486 - 1_394.40 diff --git a/policyengine_uk/tests/test_miscellaneous_loss_properties.py b/policyengine_uk/tests/test_miscellaneous_loss_properties.py new file mode 100644 index 0000000000..e3f08a5734 --- /dev/null +++ b/policyengine_uk/tests/test_miscellaneous_loss_properties.py @@ -0,0 +1,72 @@ +"""A miscellaneous loss is relieved only against miscellaneous income +(ITA 2007 s. 152(4)). + +Invariants: + +1. For any pay, savings, dividends and pension income, a miscellaneous loss + leaves Income Tax, adjusted net income and total income exactly as they are + with no miscellaneous income. +2. For any pay, positive miscellaneous income is taxable in full. +""" + +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + + +def _calc(misc, pay, savings, dividends, pension): + situation = { + "people": { + "p": { + "age": {"2026": 50}, + "employment_income": {"2026": pay}, + "savings_interest_income": {"2026": savings}, + "dividend_income": {"2026": dividends}, + "private_pension_income": {"2026": pension}, + "miscellaneous_income": {"2026": misc}, + } + }, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"], "country": {"2026": "ENGLAND"}}}, + } + sim = Simulation(situation=situation) + return { + name: float(sim.calculate(name, 2026)[0]) + for name in ( + "income_tax", + "adjusted_net_income", + "total_income", + "taxable_miscellaneous_income", + ) + } + + +amounts = st.floats(min_value=0, max_value=150_000, allow_nan=False) + + +@settings(max_examples=25, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@example(loss=20_000, pay=110_000, savings=0, dividends=0, pension=0) +@given( + loss=st.floats(min_value=1, max_value=500_000, allow_nan=False), + pay=amounts, + savings=st.floats(min_value=0, max_value=20_000), + dividends=st.floats(min_value=0, max_value=20_000), + pension=st.floats(min_value=0, max_value=40_000), +) +def test_a_miscellaneous_loss_changes_nothing_else( + loss, pay, savings, dividends, pension +): + with_loss = _calc(-loss, pay, savings, dividends, pension) + without = _calc(0, pay, savings, dividends, pension) + assert with_loss["taxable_miscellaneous_income"] == 0 + assert abs(with_loss["income_tax"] - without["income_tax"]) < 0.01 + assert abs(with_loss["adjusted_net_income"] - without["adjusted_net_income"]) < 0.01 + assert abs(with_loss["total_income"] - without["total_income"]) < 0.01 + + +@settings(max_examples=6, deadline=None, suppress_health_check=[HealthCheck.too_slow]) +@given(income=st.floats(min_value=0, max_value=100_000, allow_nan=False), pay=amounts) +def test_positive_miscellaneous_income_is_taxable_in_full(income, pay): + result = _calc(income, pay, 0, 0, 0) + assert abs(result["taxable_miscellaneous_income"] - income) < 0.01 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py index 73a93702a1..9bf59a96a5 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py @@ -35,7 +35,10 @@ def formula(person, period, parameters): # The deductions are the person's own and come off only their own # earnings, so one partner's tax, NI or pension contributions never # reduce the other partner's earned income. - earnings_components = ["employment_income", "miscellaneous_income"] + # Miscellaneous income as income tax counts it, so a loss does not + # reduce pay (and matches uc_income_tax_on_earnings). Whether it is + # earnings at all is issue #1961. + earnings_components = ["employment_income", "taxable_miscellaneous_income"] bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: earnings_components.append("basic_income") diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.py index 30f407bf45..ce8be05b12 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_miscellaneous_income.py @@ -5,11 +5,31 @@ class taxable_miscellaneous_income(Variable): value_type = float entity = Person label = "Amount of miscellaneous income that is taxable" - definition_period = YEAR - reference = dict( - title="Income Tax Act (Trading and Other Income) 2005, s. 574", - href="https://www.legislation.gov.uk/ukpga/2005/5/section/574", + documentation = ( + "Miscellaneous income charged to income tax. On a claim, a loss on a " + "miscellaneous transaction is relieved only against relevant " + "miscellaneous income: income under the same section 1016 provision as " + "the loss (ITA 2007 s. 152(4)-(5)). It is never set against other " + "income, so a net loss counts as nil here. The model holds one net " + "figure per person, so it assumes the items netted inside " + "miscellaneous_income can be relieved against each other; that can " + "undertax a person with a gain under one provision and a loss under " + "another. The input also covers income from sources outside section " + "1016, some of which carry no loss relief at all. Carrying a loss " + "forward against later years' miscellaneous income is not modelled." ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005 s. 574", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/574", + ), + dict( + title="Income Tax Act 2007 s. 152", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/152", + ), + ] unit = GBP - adds = ["miscellaneous_income"] + def formula(person, period, parameters): + return max_(0, person("miscellaneous_income", period)) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py index f13a71fb41..0503c8447c 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py @@ -20,5 +20,6 @@ class total_income(Variable): "property_income", "savings_interest_income", "dividend_income", - "miscellaneous_income", + # A miscellaneous loss is never part of total income (ITA 2007 s. 152(4)). + "taxable_miscellaneous_income", ]