From 07922ca884fc878627a5f0525bd1b631081b55b6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 26 Sep 2026 23:40:46 -0400 Subject: [PATCH 1/3] Stop stacking the trading allowance on expenses already netted from profit self_employment_income is profit after expenses (FRS SEINCAM2; SPI PROFITS "gross profits assessable"), but trading_allowance_deduction subtracted min(1000, profit) from it. Under ITTOIA 2005 Part 6A the allowance is measured against gross receipts (s. 783AC) and replaces actual expenses and capital allowances (ss. 783AE-783AF full relief, s. 783AI partial relief; HMRC BIM86050), so the old formula gave up to 1,000 of double relief to every self-employed person with profits above the allowance. The deduction is now the allowance's excess over actual expenses and capital allowances, capped at profit, when the new optional self_employment_gross_receipts input is supplied. Without receipts, a profit within the allowance gets full relief and a larger profit gets none. The allowance parameter now starts in 2017-18 (F(No.2)A 2017 Sch 3 para 13). Fixes #1880 Co-Authored-By: Claude Opus 5.5 --- changelog.d/1880.fixed.md | 1 + docs/book/programs/gov/hmrc/income-tax.ipynb | 4 +- .../allowances/trading_allowance.yaml | 9 +- .../gov/hmrc/income_tax/allowances.yaml | 11 +- .../trading_allowance_deduction.yaml | 117 +++++++++++++ .../bases/taxable_self_employment_income.yaml | 12 +- .../charges/child_benefit_hitc.yaml | 4 +- .../pension_contributions_relief.yaml | 8 +- .../test_trading_allowance_properties.py | 163 ++++++++++++++++++ .../allowances/trading_allowance_deduction.py | 49 +++++- .../input/self_employment_gross_receipts.py | 22 +++ pyproject.toml | 1 + uv.lock | 92 +++++++++- 13 files changed, 469 insertions(+), 24 deletions(-) create mode 100644 changelog.d/1880.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_deduction.yaml create mode 100644 policyengine_uk/tests/test_trading_allowance_properties.py create mode 100644 policyengine_uk/variables/input/self_employment_gross_receipts.py 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..e1247218d2 100644 --- a/docs/book/programs/gov/hmrc/income-tax.ipynb +++ b/docs/book/programs/gov/hmrc/income-tax.ipynb @@ -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..faab8ff2c8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_deduction.yaml @@ -0,0 +1,117 @@ +# 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 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..5dd65e4f7e --- /dev/null +++ b/policyengine_uk/tests/test_trading_allowance_properties.py @@ -0,0 +1,163 @@ +"""Property tests for the trading allowance (ITTOIA 2005 Part 7A, 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( + amount, # profit, net of actual expenses + amount, # actual expenses + st.sampled_from([0.0, 0.0, 150.0, 900.0, 5_000.0]), # capital allowances + st.booleans(), # whether gross receipts are supplied + 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}, + } + 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, known, extra = map(np.array, zip(*drawn)) + profit = np.concatenate([profit, profit + extra]) + expenses = np.concatenate([expenses, expenses]) + capital_allowances = np.concatenate([capital_allowances, capital_allowances]) + known = np.concatenate([known, known]) + receipts = np.where(known, profit + expenses, 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.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) + return dict( + profit=profit, + expenses=np.maximum(receipts - profit, 0), + capital_allowances=capital_allowances, + receipts=receipts, + known=receipts > 0, + 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): + """Taxable trade profit under Part 7A when receipts are known. + + Receipts within the allowance are fully relieved. Above it, the person + takes the better of actual deductions (expenses and capital allowances) + and the allowance; the two never stack. + """ + return np.where( + receipts <= ALLOWANCE, + 0, + np.maximum(0, receipts - np.maximum(expenses + capital_allowances, ALLOWANCE)), + ) + + +def unknown_receipts_taxable_profit(profit, capital_allowances): + """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)) + + +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() + assert (r["relief"] <= r["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"]), + unknown_receipts_taxable_profit(r["profit"], r["capital_allowances"]), + ) + 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) + 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..814103d366 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,50 @@ 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." + ) 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 cannot be below a positive profit, so zero means unknown. + receipts_known = receipts > 0 + 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..bc48b03810 --- /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. 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" diff --git a/pyproject.toml b/pyproject.toml index c8744633be..5a1371993f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ dev = [ "snowballstemmer>=2,<3", "jupyter-book>=2.0.0a0", "rich", + "hypothesis>=6", ] [tool.pytest.ini_options] diff --git a/uv.lock b/uv.lock index dafd4abb17..27beb8278a 100644 --- a/uv.lock +++ b/uv.lock @@ -603,6 +603,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/7b/98daa50a2db034cab6cd23a3de04fa2358cb691593d28e9130203eb7a805/huggingface_hub-0.33.4-py3-none-any.whl", hash = "sha256:09f9f4e7ca62547c70f8b82767eefadd2667f4e116acba2e3e62a5a81815a7bb", size = 515339, upload-time = "2025-07-11T12:32:46.346Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/31/b54cd138ee36a32d0047d40fe7bdcb63cefd57ce098a26024161332d15fd/hypothesis-6.168.1.tar.gz", hash = "sha256:fd8acb5c67f260dfbdcc1b2dc95ff48b2b3f90708ae595c76f591bbf3f6f8eeb", size = 510823, upload-time = "2026-09-23T04:36:04.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/95/3091b9ad24baa9666d49feea433a8c4060ad47245f0d529d736af3672ddd/hypothesis-6.168.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5d9cc2d2a4779c92788389564fc6760df7c2909185f6431378133dea6933194f", size = 791345, upload-time = "2026-09-23T04:35:39.379Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b3/b954ffa69f91d111f801030c28d6721f422222af245a846571fc87874242/hypothesis-6.168.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d0e1ec15d32af11da6784d2506de0c901e4c1abf97a3cdcfc144225457783f4e", size = 787104, upload-time = "2026-09-23T04:35:21.799Z" }, + { url = "https://files.pythonhosted.org/packages/e5/dc/732df08f570d845a480d3729bfb46beccc441bfabe2804bb3a458649be73/hypothesis-6.168.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cc38519a57c4b919b2c59714a67501d20b46b0c96a0078cbdc9f86e728efce3", size = 1123848, upload-time = "2026-09-23T04:33:48.857Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e1e937df42243267ba25ede5e9b29921186f290f6c0515729481cc41726e/hypothesis-6.168.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e229e937e761830c7fa6437fb12cb801616a1737af0898abf5edba57d2ad691", size = 1147698, upload-time = "2026-09-23T04:35:33.439Z" }, + { url = "https://files.pythonhosted.org/packages/be/d1/3b41c008dc2a49e43bc4893dcd27ea9d9fef64fef50382ca5f7107680b1f/hypothesis-6.168.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:569ba21e5543840895d17633cc6716a9578ac83b1193a2978b3f24cc16b93085", size = 1149280, upload-time = "2026-09-23T04:35:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/ea/d54781058fe6a487b81ec6c636f3806fff6a417b48cf3fcd2b003d80f3ce/hypothesis-6.168.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a232665716607c3c50c9a3aeeac5929be1d4e58703f5e7cb51db7099445a8c6", size = 1191693, upload-time = "2026-09-23T04:35:57.896Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f2/43bdd07450978611d25989ffdcbe1579acd46c1500a484f8838359fe7880/hypothesis-6.168.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9022fe330d8839259da9b496573c4b83ea248bf32029eba23ff589f3c25754bf", size = 1169725, upload-time = "2026-09-23T04:34:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/d1/72/fd4b657cba66940f208bc0eccaceaa777553009d26e86068ff3ab5e55b0b/hypothesis-6.168.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:69be660b831d55ab35619b30abd311a6cb8cd0068a1192210d194a776b211094", size = 1129181, upload-time = "2026-09-23T04:35:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/516a29d7a2bd31e28972de635350eab086c94faa07afcdd3ad0f06e70db8/hypothesis-6.168.1-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e2cccca80ee4ffe8f66edf5b2adb2a16b8cd3c5110b3a774d70f2fc75a42aa", size = 1160162, upload-time = "2026-09-23T04:35:29.333Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/e2c7e32f281b31d4c19cc2fa935bfdde5b31ad4848a485e5ebb8de2389da/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:11cbd2a2539194f0960de4ed9f980882e3a3f55c476afcc6f4cc342eec5a07d2", size = 1299714, upload-time = "2026-09-23T04:35:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bc/85850bdb1fc35d0fa9e299ca3b2e354f403959666c7ef4d0bfda9c308eee/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:aa1620111616c660a1db03347b1790b5914ef30f86c6e8ddd6c765785291f0b4", size = 1425335, upload-time = "2026-09-23T04:35:23.557Z" }, + { url = "https://files.pythonhosted.org/packages/40/c0/66bdb24f32d32aa5636655b6037af07962118b89fae16ee74bc9a1bb982e/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:a07c8665c95bb856f99ecd60b6f50d94dcf8157415387c32dcd5bb5b5061d032", size = 1376916, upload-time = "2026-09-23T04:34:59.742Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/7ad0b17bda81c473cbc539009148f3e0950880c4618028691bcf62eeb8d2/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:8a50b6335d5fa2312dd9f5d50faeb953e61475b301d2c073cd7a4c5efd7e6171", size = 1280975, upload-time = "2026-09-23T04:34:21.966Z" }, + { url = "https://files.pythonhosted.org/packages/e2/01/8d04d1eecf03a6097e26400e8393eff7a81fd52c877a664ee4e97adf7880/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:4ccb0276db8ad197a63dff25c4868e11123346347df6900625d2b1f794ce7c19", size = 1300238, upload-time = "2026-09-23T04:34:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/f5/74/eda0c70b3845789c713fe2ac72d3d4586da83f38a34f1f7aefbda0b5b01e/hypothesis-6.168.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85ca7788574e06d0591d51e19ecbb1d09bc82d50c6eae5de0fe8317590b1e138", size = 1336088, upload-time = "2026-09-23T04:33:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/80/c9/43d8528e6d43eebb7688d36e530d1fc7763809b8f923b62cc8b4d95f99ff/hypothesis-6.168.1-cp310-abi3-win32.whl", hash = "sha256:77378e04aa47a22a615af80f42970dc44782f02b8a82ac105b619a2cc7f5eee2", size = 678004, upload-time = "2026-09-23T04:34:29.564Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/f7d4de5b57972f7f68a6c7fb5698f60425589eae6fcb175f285f2525e8c0/hypothesis-6.168.1-cp310-abi3-win_amd64.whl", hash = "sha256:f94d3dbc31112f8fbcb3af19a662ee5e90e380e6ae676357903caa14a1604763", size = 684722, upload-time = "2026-09-23T04:34:41.279Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/528eb221a82da0aa477e34c7af6ed2b4a5109b326638d4418b9e49435e96/hypothesis-6.168.1-cp310-abi3-win_arm64.whl", hash = "sha256:9bd7e8c6e2c3f6a5befee6b03ebd2170da2e50658075c6decad184777c72a726", size = 682711, upload-time = "2026-09-23T04:33:46.085Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1b/620484ba32fbb655ccbd02f3c510e7a961e840a466c3672ef0b877ab8716/hypothesis-6.168.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9fd235c4ccfefe18fd97c181bdf247187cb3d4fdc8800a4d623322234ce4cfd3", size = 792049, upload-time = "2026-09-23T04:34:28.21Z" }, + { url = "https://files.pythonhosted.org/packages/03/df/2ad250c2c4ee4d5f43d6d9bc5d852e7b81c771e4c8946212931b8cafe849/hypothesis-6.168.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0722212cf825d001a0d3b4db9303cc31af467f6814b69acab91d91c3f629c95a", size = 787932, upload-time = "2026-09-23T04:35:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/24/69/4f8e8bf1d5ad81352a05aa0835baef82e691e3e43933bafe6d6f2c171763/hypothesis-6.168.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94a1053575f133ff6978876fd4b4c2a153c1111f1b5f14d6cfb355c306fc4cb5", size = 1124000, upload-time = "2026-09-23T04:34:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e7/b72a34e0c6b434d3ad2b2e0040fa88d06a15a7880c59bddacf1e27d4902a/hypothesis-6.168.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2c74d496304574cfed537e4f657620be7fee3e287381c0aa4b481211e6ec92", size = 1170172, upload-time = "2026-09-23T04:34:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6c/afe1b73b58479ba0d75e5408259978d580e96fee812eb4ea2bb83a76f7ff/hypothesis-6.168.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf4cc6819ba9689effba38dce98a3025554669f76be07e47cc1002e93e8c4918", size = 1300163, upload-time = "2026-09-23T04:35:53.549Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3d/aad54ffe7942c06da79c719fc98daa1dffdd8ac2d384ca64cc3611b1a833/hypothesis-6.168.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7888e0876e1d93fb92a6a4869834b0d0de18f07f6600edbf3d1cad16cf089357", size = 1336274, upload-time = "2026-09-23T04:35:10.655Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cb/8d2a3474e7921cfb5e59f933ac6fe1ad83b95cc98a871af8f0a68dcf946e/hypothesis-6.168.1-cp311-cp311-win_amd64.whl", hash = "sha256:4aa312b6447743a72283698292ea0e1c4d684279b38dc7f270729c4efda6c27a", size = 684479, upload-time = "2026-09-23T04:33:59.307Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6f/9d5ae55d81e10f0fc46868c19ab9d468e9959e82ff8cc8c26990b16114f3/hypothesis-6.168.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:dfe14a88b1ed03ce47333d800cc37949da73feb0cd9767e7ef076d2202768928", size = 793124, upload-time = "2026-09-23T04:33:52.589Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/54c19d259d7f92176aa2199c90f7951e4e396a26898a596dc875ccb9c389/hypothesis-6.168.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8acd000c3bf233cdcd28c1ff03f1bf56cb03821b1c742d00d57978b20fc6e6f8", size = 784674, upload-time = "2026-09-23T04:34:38.137Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ae/f80a9810f88bd654ab0d0ed0652c274e4c0384e22d4f9442b6753b9200a6/hypothesis-6.168.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29280d4743546ddcf74644f9acdc5726972cba9182699a86b7e77f3f8f6e5dde", size = 1122860, upload-time = "2026-09-23T04:33:51.375Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5d/aa5b7ffd57ec475a9252c55985c07a366823c3f85267d44648e94c1ec321/hypothesis-6.168.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd9a488d46b8c7da3103058c7569376a3a08994aee9a7d6f1d160d3d3b49b82d", size = 1168922, upload-time = "2026-09-23T04:34:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/4862448192d4c9cc61a3b8a620730800f56b1e83b0bfda63dbc728526eb3/hypothesis-6.168.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451ee6c42955887a9e1850c53813e31182eab1663e4d3dacea4e535dedc45b5d", size = 1298568, upload-time = "2026-09-23T04:35:31.191Z" }, + { url = "https://files.pythonhosted.org/packages/15/ad/55e73fda9afbfcf3336b2eee564833bc9a4fee1f7a32fe5f9d183742da39/hypothesis-6.168.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bc2c036f4a9534af64977e38b2976295136f83417fdb95bed816a52cc3023497", size = 1335091, upload-time = "2026-09-23T04:34:23.42Z" }, + { url = "https://files.pythonhosted.org/packages/ef/99/b6340fa21a89b0088ed023fe984109e1c47a07dfd741fd6d8e17a9e2795f/hypothesis-6.168.1-cp312-cp312-win_amd64.whl", hash = "sha256:d603d7c96d030a63701b6ad9bff0bc4870e4c226335b3db509f1eed1f7fc061b", size = 682011, upload-time = "2026-09-23T04:34:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/f5/23/093b9dc768e89d2cec74d29115f4004fe1be64218bac65f8d817d727d30b/hypothesis-6.168.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aebf7e9d7ba9f920e154abc65851d4affab497daeaea285474bed93840a983ea", size = 793062, upload-time = "2026-09-23T04:33:58.177Z" }, + { url = "https://files.pythonhosted.org/packages/df/d7/f191686b44b3f892ae1404e238a55260152a645a2deafe8b2949c7e90fe9/hypothesis-6.168.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b26616604cac458dd83e75900fefd9190f1074e67d5d9dc4d9e321d30f295eca", size = 784518, upload-time = "2026-09-23T04:34:02.012Z" }, + { url = "https://files.pythonhosted.org/packages/10/25/b7a5dca1911c9012a564d0b7aaeb44e393ade9424a695dea179ff32d79fe/hypothesis-6.168.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb6d89cff2edcd45e5af957c7b6a71ae7887419561f7b9c7055e1c50dea41017", size = 1122844, upload-time = "2026-09-23T04:35:06.897Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/ddacd0d244e6719ea9a9d322d4d4b6b054fd27ada94ec7aa7dce7ef11297/hypothesis-6.168.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a4ce6310ec21000446ae75cdc6667b81d4ee0f2d226a0dfaf8baca42dd151d0", size = 1168779, upload-time = "2026-09-23T04:33:56.885Z" }, + { url = "https://files.pythonhosted.org/packages/dc/07/343bab6676a41f38cb7c5912ab3ca059258bf40e41c181d1b77cc7251801/hypothesis-6.168.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a22c6e730c327d22a4d61b53f66639cdf6f0fe4a5a1b3595c36533e5b8127169", size = 1298434, upload-time = "2026-09-23T04:34:48.576Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/4b72fafa5f46a455024a9242b62505b1a53fad95ace162e831c4ffb233b9/hypothesis-6.168.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcd66493fe180d38641b02dd0456cc1e429655f8940dd1396ab874808f16dd34", size = 1334975, upload-time = "2026-09-23T04:35:01.531Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/48643915cda400ca2dccb9bcdc363690b13bcd5c179172bc6450c3d76ce8/hypothesis-6.168.1-cp313-cp313-win_amd64.whl", hash = "sha256:a8a96f2e9fa2a57f95865aaf092943775e7ea96f72535371b2be20fe5464298c", size = 681978, upload-time = "2026-09-23T04:36:00.056Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0e/e6999cddbaef975ecc1b0a7e9028a3d1a1014b542fa003889d0cb3870fa9/hypothesis-6.168.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:35b6582ecc22ed682ea39bbf972f8b8b1798043a3bc624ae1dce02d42fcf3ad4", size = 793097, upload-time = "2026-09-23T04:35:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f3/2ff6bf4bf43ab12641b163bd8fea4b6cddc9a0f07499c4779519099034c9/hypothesis-6.168.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48e467c73b7fb46bc072323c5d67e8e683cf32218fbba47c4e6ee0fe0d72687f", size = 784662, upload-time = "2026-09-23T04:35:08.976Z" }, + { url = "https://files.pythonhosted.org/packages/10/3a/b3653de877981cdab6861476c3b68d02b7c682c2875557cff7b92dbde068/hypothesis-6.168.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c82fcc3dece367bd43f3eca5c2746cf60b73f61bba071ef9421d4cfd6188a85", size = 1123106, upload-time = "2026-09-23T04:34:26.476Z" }, + { url = "https://files.pythonhosted.org/packages/49/25/24c725cb94c8953a98d8bdec83971f76c8f4b103070df1fdacd8b7f37253/hypothesis-6.168.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36031c8f8fd4bec7185cf70ed080ba54d45c106cf4f575bc8c2482c01f5f9b21", size = 1168942, upload-time = "2026-09-23T04:35:41.237Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e4/cbe45b9e78e4dd9c18ed2328ff95f659f59235b04ae3c73cf7e9c498adc9/hypothesis-6.168.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ddfe21791579fdc90423973c733578efa23b010cbfb685dcd5201cf7b0a928b6", size = 1298937, upload-time = "2026-09-23T04:33:50.181Z" }, + { url = "https://files.pythonhosted.org/packages/93/e9/cacaa3f58ee6eb98ce29a2182483d57686011bba35c8fb8a9bc8bf981c7f/hypothesis-6.168.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ecf86b1c9c8700f6d45324becbbe25dd581c69dc03e1357a64f4bf9a1afe823f", size = 1335198, upload-time = "2026-09-23T04:36:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/31/d8/ca51626959f59a557656cf8024d9afda4d213257c170412fa3193d11e11f/hypothesis-6.168.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:d5ab336a7dd233858bc3066a69479e295da788cef345d7e4efd7b77dad724db0", size = 624105, upload-time = "2026-09-23T04:34:08.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/61/6ce96e7d58fc7a592ad7c84d8c7cf9ea36db51468b30ac8306bbf4c3e6ba/hypothesis-6.168.1-cp314-cp314-win_amd64.whl", hash = "sha256:63244ba76464202d3d24126d2e4c2c1abf36db60fffe16ebaa93292572c2a42a", size = 681859, upload-time = "2026-09-23T04:34:50.801Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b1/6c7345e2b37ea8a1df2520a0ecfbdc0243a897895ff950309ca7f687429a/hypothesis-6.168.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6cec76c8cf76738ad4c0c0c5d6e4632a32d74748639ea6ce26fa672490d9bf95", size = 791685, upload-time = "2026-09-23T04:33:54.243Z" }, + { url = "https://files.pythonhosted.org/packages/72/8c/4f6cfafde4b31add263d6a562f615939cd49649e87a3695ac8693506c811/hypothesis-6.168.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9da10ba2dbbcaba93c6a11eb394fd1a0cfee6e2ba21345c3d6f2fc7401d80670", size = 783237, upload-time = "2026-09-23T04:34:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/a0/25/646c301a9d1fe3825a2ff8362594c4aee0ceb2ce192f49469f7f2824a742/hypothesis-6.168.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3218c6d18a041c0eba0cccd88b20d74be24687e42a73338abd0a31ec5294c265", size = 1121414, upload-time = "2026-09-23T04:34:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/27/4f/f84ec8434ef765e6a5c4f9942bbb7652be444b23b9ff868da43e96e61e86/hypothesis-6.168.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760cad0526a84122bbe2a0157217e16d76d649d333422142853c7f1ba43ca44d", size = 1167536, upload-time = "2026-09-23T04:33:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/38/74/c53334d793b0c54054ffc781cdf79ccae6ee51daf347bd9cfa7e0015cece/hypothesis-6.168.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50a60b57b485dff68b56f99e4506b7f1b52994f801ee1aaa4cb9125a90bf7834", size = 1297122, upload-time = "2026-09-23T04:34:17.235Z" }, + { url = "https://files.pythonhosted.org/packages/43/e7/b115a1faea6d40bddce6342da703b7536d803d2384fa50d277dc55170c64/hypothesis-6.168.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:68017d433ca5c558c34e3308a6653fc63077749af8bf86c10850cb6bb3cb559f", size = 1334055, upload-time = "2026-09-23T04:35:37.298Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/f272620d304dd67ff7815509745e2fb8d2bf58e4e1da4b7842a83f1de2da/hypothesis-6.168.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ff92099a0185ba14bf4bc2111299b456074596199148ae84590eee4d480fac2e", size = 681804, upload-time = "2026-09-23T04:35:35.374Z" }, + { url = "https://files.pythonhosted.org/packages/54/e9/45595951f8bc9bd03f032325e7b176ca990a0c86bf2e09d481e8ee5a5cab/hypothesis-6.168.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:e9510e33467d586a7700d56b9a6ee65b44a06832c1cab3d270369d7c53381276", size = 791046, upload-time = "2026-09-23T04:35:55.724Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3e/718431d0ab72c4c07454f71040b4de298a63e765f20088557dc1bef0a48b/hypothesis-6.168.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:99970e3a75f11dde0414d3d6724bd8b7a2bd1deeb9a509ec9236c91d41a5cff0", size = 782982, upload-time = "2026-09-23T04:34:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/d7/05/c60db6df1456a70a3c589e42acaa446c22f56ed342c1b8dd04b8e49df8fd/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5ea78773de5bcc25266e37eb086472e90a85fc81b8d4a9ca8afc15858ee15fb", size = 1120961, upload-time = "2026-09-23T04:33:43.494Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fe/d943250395750ecc3b4ec95b1ab3c8119f6cbad08c1ba9169ffee7d89049/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13f502ce2423e46dfcd2af38950a86ecf97084d94e721f7f13dd5a70afb36e0a", size = 1143877, upload-time = "2026-09-23T04:34:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9d/8c1f8e11f20fd81ce40caed7ad6d6a9776e24775070e87f3aa9ba3683272/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b4bcaa8b0e753fc8921011d5d10c8a7c3caf814c6a968bc140a08917973fdc", size = 1146503, upload-time = "2026-09-23T04:34:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/71/d8/ac2e9fa653d2f05a13724837007e857d376d81fdf728543ab85f6c7f7350/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ed811908d38ca8e05c20ad25ecb61edc1e20a519667d4b668be49dd09e6bf3e", size = 1189216, upload-time = "2026-09-23T04:34:36.271Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7b/640952448781c7df5098880e2e2d6e7702b3a1c0badd4c0ea5fca6d6ab53/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9239c868418088d58ae18d041f9f424bd7a75dd143f209c35fde38770b65f74f", size = 1166919, upload-time = "2026-09-23T04:35:16.498Z" }, + { url = "https://files.pythonhosted.org/packages/b6/de/0d46b330a28f7614850fb1a0564cf88af7c0b29cbd791e9d04ade7f66f13/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:5cac279798ecc92d3301ec4421c0458e22627f8a79fdde8a6c98ab1d4b8811f6", size = 1126652, upload-time = "2026-09-23T04:35:49.432Z" }, + { url = "https://files.pythonhosted.org/packages/92/36/6adeca1724ed4966095406ffe7308fcff338cf9f5b7443d5ea34a01ab6e1/hypothesis-6.168.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bc660dc14d633641e22e407417d35bf2925331273ed71bc6ab5536dfca5c72d", size = 1155687, upload-time = "2026-09-23T04:35:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/bd/00/e9b7c7e4a0a75d1621be95a126999352e01a16d68b0a59b396695c7b4991/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:157a29e4d6bcb77ae732cb83d6546beef47265ecac88408fdd5d5b2bd325685d", size = 1296471, upload-time = "2026-09-23T04:34:12.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/52cb320e9e07cc67f718506617b5900344370394e39f4affada317aab313/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:b125aac89594ee2a76687b724f057b2423ea83dfee6b8ff59f54289d6eef19a1", size = 1421838, upload-time = "2026-09-23T04:34:20.451Z" }, + { url = "https://files.pythonhosted.org/packages/9b/93/969c33a87cd2865beffebe491294b8115e7e7421c6840f79a67486535bfc/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:4b960c9d31b85dbc6a25888897c02bbea5f498844932841612925184fc78d9eb", size = 1373994, upload-time = "2026-09-23T04:34:00.608Z" }, + { url = "https://files.pythonhosted.org/packages/34/c9/5fe4ade4bd23baa2e4cd27668bb8ac10e840ce19fbcfc74116ea529cbaa7/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d4d76f35874471fe9b8e1bb696fb532363ef3d9588161a06d30520f638f15a39", size = 1278216, upload-time = "2026-09-23T04:34:07.253Z" }, + { url = "https://files.pythonhosted.org/packages/a1/17/30b9a1ad975e90700dea74ce8b08ab07b9e58c4d83918422bc8841942c1e/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:d5147c5f7497d109eed0267c498adaf5cad6e4beeaca6fad6057f2007bae70a7", size = 1297647, upload-time = "2026-09-23T04:35:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2f/233a9d5adafad6e187ab8f4d9275592cf120b56e63ea1cfb02213c01021e/hypothesis-6.168.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:a909f89758e184c4f86418598de297a2ced5b922979c0f98c5d73ff6bafe38e5", size = 1333790, upload-time = "2026-09-23T04:34:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/10/d7/4c9d4d597f70316d781cd99b600adae090a6f5d675487196e10510a23afe/hypothesis-6.168.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:66b9fb6f02840f8ffa2a4e6359aff6ecc1b2c6630c8a2066771044fa22956c7a", size = 675206, upload-time = "2026-09-23T04:35:14.592Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f9fc0c0dbb9e80238fc7145cb4ce52e0897ea417a23217022911855bccda/hypothesis-6.168.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9246eaa80044c33a182be661cbed7e6a7314c61359ef501ea9e9efd90d50ca3b", size = 681503, upload-time = "2026-09-23T04:34:11.223Z" }, + { url = "https://files.pythonhosted.org/packages/11/b4/962ae350c10860be37c9abf98c0ef1ef23e4baca3ee7ad77208941e370e5/hypothesis-6.168.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:52e0c804cbf697d4760400c84444a91cbd5dfe7ae0ef1bb3228f74fb5547eb9e", size = 679199, upload-time = "2026-09-23T04:34:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c5/2bfdcc601b08eb9611bc524d18ca0a8cdbbdd7bd07bc68c38e0196a00668/hypothesis-6.168.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:91e3700d9c35e184dd253cff2f151f890557242bede75a2535901e9062d42975", size = 792943, upload-time = "2026-09-23T04:35:25.308Z" }, + { url = "https://files.pythonhosted.org/packages/4b/52/36ae5128d9102d73dc578cd7b241c33f5483bd56e05799a2d06043a1ce7d/hypothesis-6.168.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c19bc5da324a5899527ea749522e9ceb47f1eb54772869673abe59ae0b703aef", size = 788802, upload-time = "2026-09-23T04:34:39.655Z" }, + { url = "https://files.pythonhosted.org/packages/23/34/698fc3481757503dc60f07547c29098dce97ac035a1ee134d66d88814281/hypothesis-6.168.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a4c9e46b2f349658d3013a357e1fbf114bc08abced7ee8e91cd6e0bc718b195", size = 1124766, upload-time = "2026-09-23T04:34:57.869Z" }, + { url = "https://files.pythonhosted.org/packages/02/c9/1f871666106d90048027fc6b309ec505f0fb44e4519add73ddaad98bcd9a/hypothesis-6.168.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a0beee16fcbc0516951f2ef3ef1a479ad11d9726da49840139303b36ebc1d0", size = 1171766, upload-time = "2026-09-23T04:34:54.476Z" }, + { url = "https://files.pythonhosted.org/packages/17/44/eff662526259ac4447dde4abe4fc285d0e8e5ff5345d35af836c6f0f12cc/hypothesis-6.168.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4a8beb513c066fcb187b885fdfa93da4006f223d50d605b687bfb7e60aac33e9", size = 685449, upload-time = "2026-09-23T04:33:44.837Z" }, +] + [[package]] name = "idna" version = "3.15" @@ -1383,7 +1471,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.93.1" +version = "2.102.0" source = { editable = "." } dependencies = [ { name = "microdf-python" }, @@ -1396,6 +1484,7 @@ dependencies = [ dev = [ { name = "coverage" }, { name = "furo" }, + { name = "hypothesis" }, { name = "jupyter-book" }, { name = "pytest-cov" }, { name = "rich" }, @@ -1412,6 +1501,7 @@ dev = [ requires-dist = [ { name = "coverage", marker = "extra == 'dev'" }, { name = "furo", marker = "extra == 'dev'", specifier = "<2023" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0a0" }, { name = "microdf-python", specifier = ">=1.2.1" }, { name = "policyengine-core", specifier = ">=3.30.1" }, From 113c6f7a8edec8db8a8792d99be9544192765630 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 26 Sep 2026 23:49:22 -0400 Subject: [PATCH 2/3] Address review: guard inconsistent receipts, widen property tests - Treat gross receipts below profit as unknown rather than as zero expenses. - Document the discontinuity at the allowance when receipts are unknown. - Property tests now draw losses, loss relief and receipts below profit, and fix the Part 6A reference in the docstring. - Refresh the saved docs notebook output for the trading allowance row. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/hmrc/income-tax.ipynb | 4 +- .../test_trading_allowance_properties.py | 72 +++++++++++++------ .../allowances/trading_allowance_deduction.py | 8 ++- .../input/self_employment_gross_receipts.py | 6 +- 4 files changed, 61 insertions(+), 29 deletions(-) diff --git a/docs/book/programs/gov/hmrc/income-tax.ipynb b/docs/book/programs/gov/hmrc/income-tax.ipynb index e1247218d2..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", diff --git a/policyengine_uk/tests/test_trading_allowance_properties.py b/policyengine_uk/tests/test_trading_allowance_properties.py index 5dd65e4f7e..08ca9bfe31 100644 --- a/policyengine_uk/tests/test_trading_allowance_properties.py +++ b/policyengine_uk/tests/test_trading_allowance_properties.py @@ -1,4 +1,4 @@ -"""Property tests for the trading allowance (ITTOIA 2005 Part 7A, Chapter 1). +"""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 @@ -26,10 +26,14 @@ st.sampled_from([0.0, 999.99, 1_000.0, 1_000.01, 12_570.0, 50_270.0]), ) case = st.tuples( - amount, # profit, net of actual expenses + 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.booleans(), # whether gross receipts are supplied + 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) @@ -46,6 +50,7 @@ def sim(): "self_employment_income": {YEAR: 0}, "self_employment_gross_receipts": {YEAR: 0}, "capital_allowances": {YEAR: 0}, + "loss_relief": {YEAR: 0}, } for i in range(PEOPLE) } @@ -63,50 +68,66 @@ def sim(): def run(sim, drawn): - profit, expenses, capital_allowances, known, extra = map(np.array, zip(*drawn)) + profit, expenses, capital_allowances, loss_relief, mode, share, extra = map( + np.array, zip(*drawn) + ) profit = np.concatenate([profit, profit + extra]) - expenses = np.concatenate([expenses, expenses]) - capital_allowances = np.concatenate([capital_allowances, capital_allowances]) - known = np.concatenate([known, known]) - receipts = np.where(known, profit + expenses, 0) + 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, + 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): - """Taxable trade profit under Part 7A when receipts are known. +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. Above it, the person - takes the better of actual deductions (expenses and capital allowances) - and the allowance; the two never stack. + 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 - np.maximum(expenses + capital_allowances, ALLOWANCE)), + np.maximum(0, receipts - best_deduction - loss_relief), ) -def unknown_receipts_taxable_profit(profit, capital_allowances): +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)) + return np.where( + profit <= ALLOWANCE, + 0, + np.maximum(0, profit - capital_allowances - loss_relief), + ) def tolerance(*values): @@ -127,7 +148,8 @@ 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() - assert (r["relief"] <= r["profit"] + tolerance(r["profit"])).all() + positive_profit = np.maximum(r["profit"], 0) + assert (r["relief"] <= positive_profit + tolerance(r["profit"])).all() @SETTINGS @@ -136,8 +158,12 @@ 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"]), - unknown_receipts_taxable_profit(r["profit"], r["capital_allowances"]), + 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"]) @@ -151,7 +177,11 @@ def test_allowance_never_stacks_on_actual_deductions(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) + tolerance(r["receipts"]) + limit = ( + np.maximum(actual_deductions, ALLOWANCE) + + r["loss_relief"] + + tolerance(r["receipts"]) + ) assert (deducted_from_receipts[known] <= limit[known]).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 814103d366..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 @@ -15,7 +15,9 @@ class trading_allowance_deduction(Variable): "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." + "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 = [ @@ -39,8 +41,8 @@ def formula(person, period, parameters): profit = person("self_employment_income", period) receipts = person("self_employment_gross_receipts", period) capital_allowances = person("capital_allowances", period) - # Receipts cannot be below a positive profit, so zero means unknown. - receipts_known = receipts > 0 + # 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 diff --git a/policyengine_uk/variables/input/self_employment_gross_receipts.py b/policyengine_uk/variables/input/self_employment_gross_receipts.py index bc48b03810..c7ff04c188 100644 --- a/policyengine_uk/variables/input/self_employment_gross_receipts.py +++ b/policyengine_uk/variables/input/self_employment_gross_receipts.py @@ -8,9 +8,9 @@ class self_employment_gross_receipts(Variable): 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. Leave at zero " - "when unknown; self_employment_income remains the profit used " - "everywhere else." + "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 From e96dda219f52ac446ae76de00b0cfdf74a67aee8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 26 Sep 2026 23:51:32 -0400 Subject: [PATCH 3/3] Add YAML case for receipts below profit Co-Authored-By: Claude Opus 5.5 --- .../allowances/trading_allowance_deduction.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index faab8ff2c8..bb1a8391de 100644 --- 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 @@ -115,3 +115,13 @@ 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