diff --git a/changelog.d/1880.fixed.md b/changelog.d/1880.fixed.md new file mode 100644 index 0000000000..3c510e8d4e --- /dev/null +++ b/changelog.d/1880.fixed.md @@ -0,0 +1 @@ +- Stopped the trading allowance being deducted on top of expenses already netted out of self-employment profit (ITTOIA 2005 Part 6A). It now gives full relief only where profit is within the allowance, and partial relief when the new optional `self_employment_gross_receipts` input shows that expenses and capital allowances fall short of it. The allowance now starts in 2017-18. diff --git a/docs/book/programs/gov/hmrc/income-tax.ipynb b/docs/book/programs/gov/hmrc/income-tax.ipynb index 77f1fc99aa..f523dfb995 100644 --- a/docs/book/programs/gov/hmrc/income-tax.ipynb +++ b/docs/book/programs/gov/hmrc/income-tax.ipynb @@ -456,7 +456,7 @@ " Trading Allowance\n", " Trading Allowance\n", " 1000.0\n", - " 2005-04-01\n", + " 2017-04-06\n", " currency-GBP\n", " \n", " \n", @@ -484,7 +484,7 @@ "16 Personal Savings Allowance Higher Threshold 500.0 2005-04-01 \n", "17 Dividend Allowance Dividend Allowance 2000.0 2015-06-05 \n", "18 Property Allowance Property Allowance 1000.0 2005-04-01 \n", - "19 Trading Allowance Trading Allowance 1000.0 2005-04-01 \n", + "19 Trading Allowance Trading Allowance 1000.0 2017-04-06 \n", "\n", " Unit \n", "0 currency-GBP \n", @@ -648,8 +648,8 @@ "trading_allowance_data = {\n", " \"Attribute\": [\"Trading Allowance\"],\n", " \"Type\": [\"Trading Allowance\"],\n", - " \"Value\": [trading_allowance.values_list[-1].value],\n", - " \"Effective Date\": [trading_allowance.values_list[-1].instant_str],\n", + " \"Value\": [trading_allowance.values_list[0].value],\n", + " \"Effective Date\": [trading_allowance.values_list[0].instant_str],\n", " \"Unit\": [trading_allowance.metadata[\"unit\"]],\n", "}\n", "\n", diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/trading_allowance.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/trading_allowance.yaml index 5e84fde99c..bbaa6d997b 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/trading_allowance.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/trading_allowance.yaml @@ -1,8 +1,15 @@ -description: Amount of trading income untaxed per year. +description: Amount of trading allowance, which relieves trading receipts in place of deducting actual expenses. values: 2005-04-01: + value: 0 + reference: + - title: Finance (No. 2) Act 2017 Sch. 3 para. 13 (allowance has effect from 2017-18) + href: https://www.legislation.gov.uk/ukpga/2017/32/schedule/3/paragraph/13 + 2017-04-06: value: 1_000 reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 783AD + href: https://www.legislation.gov.uk/ukpga/2005/5/section/783AD - title: Finance (No. 2) Act 2017 Sch. 3 href: https://www.legislation.gov.uk/ukpga/2017/32/schedule/3 metadata: diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml index 2d774fffd5..5acbec23db 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml @@ -48,14 +48,23 @@ output: trading_allowance_deduction: 500 taxable_self_employment_income: 0 -- name: Trading allowance partial deduction +- name: Trading allowance partial deduction with no expenses period: 2020 absolute_error_margin: 0 input: self_employment_income: 5000 + self_employment_gross_receipts: 5000 output: trading_allowance_deduction: 1000 taxable_self_employment_income: 4000 +- name: Trading allowance does not stack on a profit already net of expenses + period: 2020 + absolute_error_margin: 0 + input: + self_employment_income: 5000 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 5000 - name: Property allowance full deduction period: 2020 absolute_error_margin: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_deduction.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_deduction.yaml new file mode 100644 index 0000000000..bb1a8391de --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_deduction.yaml @@ -0,0 +1,127 @@ +# Trading allowance, ITTOIA 2005 Part 6A Chapter 1 (ss. 783A-783AR). +# self_employment_income is profit after actual expenses. The allowance is an +# alternative to those expenses (s. 783AI; HMRC BIM86050), so it may only add +# the part of the £1,000 that actual deductions have not already used. + +- name: Sole trader with £25,000 profit gets no allowance on top of expenses + period: 2027 + absolute_error_margin: 0.01 + input: + self_employment_income: 25_000 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 25_000 + adjusted_net_income: 25_000 + # (25,000 - 12,570) * 20% + income_tax: 2_486 + +- name: Profit within the allowance, receipts unknown, is fully relieved + period: 2027 + absolute_error_margin: 0 + input: + self_employment_income: 600 + output: + trading_allowance_deduction: 600 + taxable_self_employment_income: 0 + +- name: Profit equal to the allowance, receipts unknown, is fully relieved + period: 2027 + absolute_error_margin: 0 + input: + self_employment_income: 1_000 + output: + trading_allowance_deduction: 1_000 + taxable_self_employment_income: 0 + +- name: Profit above the allowance, receipts unknown, is taxed as reported + period: 2027 + absolute_error_margin: 0 + input: + self_employment_income: 1_001 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 1_001 + +- name: Full relief when receipts are within the allowance (s. 783AE-783AF) + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 800 + self_employment_income: 500 + output: + trading_allowance_deduction: 500 + taxable_self_employment_income: 0 + +- name: Partial relief beats £100 of expenses (s. 783AI) + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 1_200 + self_employment_income: 1_100 + output: + trading_allowance_deduction: 900 + # 1,200 receipts - 1,000 allowance + taxable_self_employment_income: 200 + +- name: Partial relief beats £500 of expenses + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 1_200 + self_employment_income: 700 + output: + trading_allowance_deduction: 500 + taxable_self_employment_income: 200 + +- name: Actual expenses above the allowance leave no extra relief + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 25_000 + self_employment_income: 20_000 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 20_000 + +- name: The allowance replaces capital allowances as well as expenses + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 1_500 + self_employment_income: 1_500 + capital_allowances: 300 + output: + trading_allowance_deduction: 700 + # 1,500 receipts - 1,000 allowance, with no capital allowances + taxable_self_employment_income: 500 + +- name: Expenses plus capital allowances above the allowance use the profit method + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 1_500 + self_employment_income: 1_200 + capital_allowances: 900 + output: + trading_allowance_deduction: 0 + # 1,500 - 300 expenses - 900 capital allowances + taxable_self_employment_income: 300 + +- name: No trading allowance before 2017-18 + period: 2016 + absolute_error_margin: 0 + input: + self_employment_income: 600 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 600 + +- name: Receipts below profit are inconsistent and treated as unknown + period: 2027 + absolute_error_margin: 0 + input: + self_employment_gross_receipts: 500 + self_employment_income: 1_500 + output: + trading_allowance_deduction: 0 + taxable_self_employment_income: 1_500 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_self_employment_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_self_employment_income.yaml index eb0a8fe786..61fd6bf0a9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_self_employment_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_self_employment_income.yaml @@ -49,7 +49,7 @@ period: 2024 input: self_employment_income: 30000 - loss_relief: 10000 + loss_relief: 15000 capital_allowances: 15000 gov.hmrc.income_tax.allowances.trading_allowance: 5000 output: @@ -63,7 +63,7 @@ capital_allowances: 10000 gov.hmrc.income_tax.allowances.trading_allowance: 1000 output: - taxable_self_employment_income: 34000 + taxable_self_employment_income: 35000 - name: High self-employment income with some deductions period: 2024 @@ -73,7 +73,7 @@ capital_allowances: 30000 gov.hmrc.income_tax.allowances.trading_allowance: 1000 output: - taxable_self_employment_income: 149000 + taxable_self_employment_income: 150000 - name: Low self-employment income with low deductions period: 2024 @@ -83,7 +83,7 @@ capital_allowances: 200 gov.hmrc.income_tax.allowances.trading_allowance: 300 output: - taxable_self_employment_income: 400 + taxable_self_employment_income: 700 - name: Deductions exceed self-employment income period: 2024 @@ -103,7 +103,7 @@ capital_allowances: 300000 gov.hmrc.income_tax.allowances.trading_allowance: 1000 output: - taxable_self_employment_income: 499000 + taxable_self_employment_income: 500000 - name: Decimal values period: 2024 @@ -113,7 +113,7 @@ capital_allowances: 5000.75 gov.hmrc.income_tax.allowances.trading_allowance: 1000.00 output: - taxable_self_employment_income: 33999.50 + taxable_self_employment_income: 34999.50 - name: Pre-net self-employment income can override trading allowance deduction period: 2024 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/child_benefit_hitc.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/child_benefit_hitc.yaml index 03739d3610..c20990a727 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/child_benefit_hitc.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/child_benefit_hitc.yaml @@ -48,5 +48,5 @@ output: child_benefit: 2_337.40 CB_HITC: [2_337.40, 0, 0, 0] - income_tax_pre_charges: [35_142, 4_486, 0, 0] - income_tax: [37_479.40, 4_486, 0, 0] + income_tax_pre_charges: [35_742, 4_486, 0, 0] + income_tax: [38_079.40, 4_486, 0, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_relief.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_relief.yaml index bf97289922..dc3dc27731 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_relief.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_relief.yaml @@ -20,7 +20,7 @@ input: self_employment_income: 30_000 output: - income_tax: 3_286 + income_tax: 3_486 - name: £30k self-employment income, £3k private pension contributions period: 2023 @@ -30,7 +30,7 @@ personal_pension_contributions: 3_000 output: pension_contributions_relief: 3_000 - income_tax: 3286 - 3000 * 0.2 + income_tax: 3486 - 3000 * 0.2 - name: £60k self-employment income, no private pension contributions period: 2023 @@ -38,7 +38,7 @@ input: self_employment_income: 60_000 output: - income_tax: 11_032 + income_tax: 11_432 - name: £60k self-employment income, £6k private pension contributions period: 2023 @@ -48,4 +48,4 @@ personal_pension_contributions: 6_000 output: pension_contributions_relief: 6_000 - income_tax: 11032 - 6000 * 0.4 + income_tax: 11432 - 6000 * 0.4 diff --git a/policyengine_uk/tests/test_trading_allowance_properties.py b/policyengine_uk/tests/test_trading_allowance_properties.py new file mode 100644 index 0000000000..08ca9bfe31 --- /dev/null +++ b/policyengine_uk/tests/test_trading_allowance_properties.py @@ -0,0 +1,193 @@ +"""Property tests for the trading allowance (ITTOIA 2005 Part 6A, Chapter 1). + +self_employment_income is profit after actual expenses. The allowance is an +alternative to those expenses, so the model may only add the part of the +allowance that actual deductions have not already used. These tests check +that invariant against an independent statutory reference, plus bounds and +monotonicity, over many generated cases in one reusable simulation. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2027 +ALLOWANCE = 1_000 +OTHER_EMPLOYMENT_INCOME = [0, 8_000, 30_000, 60_000, 110_000, 200_000] +CASES = 24 # Each case is simulated twice: as drawn, and with more profit. +PEOPLE = 2 * CASES + +amount = st.one_of( + st.floats(min_value=0, max_value=2_500), + st.floats(min_value=0, max_value=300_000), + st.sampled_from([0.0, 999.99, 1_000.0, 1_000.01, 12_570.0, 50_270.0]), +) +case = st.tuples( + st.one_of(amount, st.floats(min_value=-5_000, max_value=0)), # profit + amount, # actual expenses + st.sampled_from([0.0, 0.0, 150.0, 900.0, 5_000.0]), # capital allowances + st.sampled_from([0.0, 0.0, 0.0, 300.0, 2_000.0]), # loss relief + # Receipts: unknown, consistent (profit + expenses), or inconsistently + # below profit, which the model must treat as unknown. + st.sampled_from(["unknown", "consistent", "below_profit"]), + st.floats(min_value=0.01, max_value=0.99), # receipts share if below profit + st.floats(min_value=0, max_value=20_000), # extra profit for monotonicity +) +cases = st.lists(case, min_size=CASES, max_size=CASES) + + +@pytest.fixture(scope="module") +def sim(): + people = { + f"person_{i}": { + "age": {YEAR: 40}, + "employment_income": { + YEAR: OTHER_EMPLOYMENT_INCOME[i % CASES % len(OTHER_EMPLOYMENT_INCOME)] + }, + "self_employment_income": {YEAR: 0}, + "self_employment_gross_receipts": {YEAR: 0}, + "capital_allowances": {YEAR: 0}, + "loss_relief": {YEAR: 0}, + } + for i in range(PEOPLE) + } + return Simulation( + situation={ + "people": people, + "benunits": { + f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + "households": { + f"household_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + } + ) + + +def run(sim, drawn): + profit, expenses, capital_allowances, loss_relief, mode, share, extra = map( + np.array, zip(*drawn) + ) + profit = np.concatenate([profit, profit + extra]) + expenses, capital_allowances, loss_relief, mode, share = ( + np.concatenate([x, x]) + for x in (expenses, capital_allowances, loss_relief, mode, share) + ) + receipts = np.select( + [mode == "consistent", mode == "below_profit"], + [profit + expenses, profit * share], + 0, + ) + sim.set_input("self_employment_income", YEAR, profit) + sim.set_input("self_employment_gross_receipts", YEAR, receipts) + sim.set_input("capital_allowances", YEAR, capital_allowances) + sim.set_input("loss_relief", YEAR, loss_relief) + sim.reset_calculations() + # Compare against the stored (float32) inputs, not the float64 draws. + profit = sim.calculate("self_employment_income", YEAR).astype(float) + receipts = sim.calculate("self_employment_gross_receipts", YEAR).astype(float) + capital_allowances = sim.calculate("capital_allowances", YEAR).astype(float) + loss_relief = sim.calculate("loss_relief", YEAR).astype(float) + return dict( + profit=profit, + expenses=np.maximum(receipts - profit, 0), + capital_allowances=capital_allowances, + loss_relief=loss_relief, + receipts=receipts, + known=(receipts > 0) & (receipts >= profit), + relief=sim.calculate("trading_allowance_deduction", YEAR), + taxable=sim.calculate("taxable_self_employment_income", YEAR), + income_tax=sim.calculate("income_tax", YEAR), + ) + + +def statutory_taxable_profit(receipts, expenses, capital_allowances, loss_relief): + """Taxable trade profit under Part 6A when receipts are known. + + Receipts within the allowance are fully relieved (profits or losses are + nil, s. 783AF). Above it, the person takes the better of actual + deductions (expenses and capital allowances) and the allowance (s. 783AI); + the two never stack. Loss relief then applies to what remains. + """ + best_deduction = np.maximum(expenses + capital_allowances, ALLOWANCE) + return np.where( + receipts <= ALLOWANCE, + 0, + np.maximum(0, receipts - best_deduction - loss_relief), + ) + + +def unknown_receipts_taxable_profit(profit, capital_allowances, loss_relief): + """Fallback when receipts are unknown: full relief only for a profit + within the allowance; a larger profit is taxed as reported.""" + return np.where( + profit <= ALLOWANCE, + 0, + np.maximum(0, profit - capital_allowances - loss_relief), + ) + + +def tolerance(*values): + """A penny plus float32 rounding on the amounts involved.""" + return 0.01 + 1e-6 * sum(np.abs(v) for v in values) + + +SETTINGS = settings( + max_examples=40, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +@SETTINGS +@given(cases) +def test_relief_is_bounded_by_allowance_and_profit(sim, drawn): + r = run(sim, drawn) + assert (r["relief"] >= 0).all() + assert (r["relief"] <= ALLOWANCE + tolerance(ALLOWANCE)).all() + positive_profit = np.maximum(r["profit"], 0) + assert (r["relief"] <= positive_profit + tolerance(r["profit"])).all() + + +@SETTINGS +@given(cases) +def test_taxable_profit_matches_statutory_reference(sim, drawn): + r = run(sim, drawn) + expected = np.where( + r["known"], + statutory_taxable_profit( + r["receipts"], r["expenses"], r["capital_allowances"], r["loss_relief"] + ), + unknown_receipts_taxable_profit( + r["profit"], r["capital_allowances"], r["loss_relief"] + ), + ) + assert ( + np.abs(r["taxable"] - expected) <= tolerance(r["receipts"], r["profit"]) + ).all() + + +@SETTINGS +@given(cases) +def test_allowance_never_stacks_on_actual_deductions(sim, drawn): + r = run(sim, drawn) + known = r["known"] & (r["receipts"] > ALLOWANCE) + deducted_from_receipts = r["receipts"] - r["taxable"] + actual_deductions = r["expenses"] + r["capital_allowances"] + limit = ( + np.maximum(actual_deductions, ALLOWANCE) + + r["loss_relief"] + + tolerance(r["receipts"]) + ) + assert (deducted_from_receipts[known] <= limit[known]).all() + + +@SETTINGS +@given(cases) +def test_income_tax_is_non_decreasing_in_profit(sim, drawn): + r = run(sim, drawn) + base, more = r["income_tax"][:CASES], r["income_tax"][CASES:] + assert (more >= base - tolerance(base)).all() diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/trading_allowance_deduction.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/trading_allowance_deduction.py index 8a7ea36ce1..6fe3203318 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/trading_allowance_deduction.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/trading_allowance_deduction.py @@ -5,15 +5,52 @@ class trading_allowance_deduction(Variable): value_type = float entity = Person label = "Deduction applied by the trading allowance" + documentation = ( + "Relief from the trading allowance beyond the expenses already " + "deducted in self_employment_income. The allowance replaces actual " + "expenses and capital allowances rather than adding to them: receipts " + "within the allowance are fully relieved, and above it the person may " + "elect to deduct the allowance from receipts instead. With gross " + "receipts supplied, the deduction is the allowance less actual " + "expenses and capital allowances, capped at profit. Without them, a " + "profit within the allowance is treated as coming from receipts within " + "it, and a larger profit as already reflecting the better of actual " + "expenses and the allowance, so taxable profit jumps from nil to the " + "full profit just above the allowance. Receipts below profit are " + "inconsistent and treated as unknown." + ) definition_period = YEAR - reference = "Income Tax (Trading and Other Income) Act 2005 s. 783AF" + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783AF (full relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783AF", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783AI (partial relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783AI", + ), + dict( + title="HMRC Business Income Manual BIM86050", + href="https://www.gov.uk/hmrc-internal-manuals/business-income-manual/bim86050", + ), + ] unit = GBP def formula(person, period, parameters): - trading_allowance = parameters( - period - ).gov.hmrc.income_tax.allowances.trading_allowance - return min_( - trading_allowance, - person("self_employment_income", period), + allowance = parameters(period).gov.hmrc.income_tax.allowances.trading_allowance + profit = person("self_employment_income", period) + receipts = person("self_employment_gross_receipts", period) + capital_allowances = person("capital_allowances", period) + # Receipts below profit (including the default zero) mean unknown. + receipts_known = (receipts > 0) & (receipts >= profit) + expenses = max_(receipts - profit, 0) + # Full relief (receipts within the allowance, s. 783AE-783AF) makes + # the profit nil; partial relief (s. 783AI) replaces expenses and + # capital allowances with the allowance. Both come to the allowance's + # excess over actual deductions, capped at profit. + relief_with_receipts = min_(profit, allowance - expenses) - capital_allowances + relief_without_receipts = where( + profit <= allowance, profit - capital_allowances, 0 ) + relief = where(receipts_known, relief_with_receipts, relief_without_receipts) + return max_(relief, 0) diff --git a/policyengine_uk/variables/input/self_employment_gross_receipts.py b/policyengine_uk/variables/input/self_employment_gross_receipts.py new file mode 100644 index 0000000000..c7ff04c188 --- /dev/null +++ b/policyengine_uk/variables/input/self_employment_gross_receipts.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class self_employment_gross_receipts(Variable): + value_type = float + entity = Person + label = "self-employment gross receipts" + documentation = ( + "Gross receipts (turnover) of the person's trades before expenses. " + "Optional: used only to apply the trading allowance, which is measured " + "against gross receipts and replaces actual expenses. Must be at least " + "self_employment_income; leave at zero when unknown. " + "self_employment_income remains the profit used everywhere else." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783AC", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783AC", + ) + quantity_type = FLOW + uprating = "gov.economic_assumptions.indices.obr.per_capita.mixed_income"