diff --git a/changelog.d/2170.added.md b/changelog.d/2170.added.md new file mode 100644 index 0000000000..021ba2b42b --- /dev/null +++ b/changelog.d/2170.added.md @@ -0,0 +1 @@ +- Added landlords' residential finance-cost tax reduction (ITTOIA 2005 ss. 272A, 274A and 274AA) from three new optional inputs: `property_rental_income` (gross receipts), `property_finance_costs` and `property_finance_costs_brought_forward`. The reduction is the property basic rate (the basic rate before 2027-28) on the lowest of the relievable costs, the property profits and adjusted total income, is limited to the tax left at Step 5, and reports the unrelieved costs as `property_finance_costs_carried_forward`. A new `disallowed_share` parameter carries the 2017-18 to 2020-21 phase-in, and setting it to nil restores full deductibility. Universal Credit sets the reduction against the tax on property income, not the tax on earnings, and the means tests that count property income (tax credits, Housing Benefit, Income Support, Pension Credit and council tax reduction) count it after finance costs. diff --git a/changelog.d/2170.fixed.md b/changelog.d/2170.fixed.md new file mode 100644 index 0000000000..2eaac93974 --- /dev/null +++ b/changelog.d/2170.fixed.md @@ -0,0 +1 @@ +- Stopped the property allowance being deducted on top of expenses already netted out of `property_income` (ITTOIA 2005 Part 6A Chapter 2). It now gives full relief only where profit, or receipts when known, are within the allowance, and partial relief when `property_rental_income` shows that expenses fall short of it. A landlord takes the allowance or the finance-cost reduction, whichever leaves less income tax. The allowance now starts in 2017-18. diff --git a/docs/book/assumptions/growthfactors.md b/docs/book/assumptions/growthfactors.md index 4d88c837b9..1a1d52e1ee 100644 --- a/docs/book/assumptions/growthfactors.md +++ b/docs/book/assumptions/growthfactors.md @@ -60,7 +60,7 @@ We derive these rates from OBR GDP growth and ONS population projections. |-------------|------|------|------|------|------|------|------|------| | Growth rate | 9.2% | 5.0% | 3.8% | 2.8% | 2.8% | 3.1% | 3.3% | 3.3% | -We use per capita GDP for these variables: `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `corporate_wealth`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `savings`, `savings_interest_income`, `sublet_income` +We use per capita GDP for these variables: `capital_gains`, `capital_gains_badr`, `capital_gains_before_response`, `capital_gains_carried_interest`, `capital_gains_residential_property`, `corporate_wealth`, `dividend_income`, `gross_financial_wealth`, `household_lifetime_isa_balance`, `lifetime_isa_balance`, `lump_sum_income`, `main_residence_value`, `maintenance_income`, `miscellaneous_income`, `mortgage_capital_repayment`, `net_financial_wealth`, `non_residential_property_value`, `other_investment_income`, `other_residential_property_value`, `owned_land`, `pension_income`, `private_pension_wealth`, `private_transfer_income`, `property_income`, `property_rental_income`, `savings`, `savings_interest_income`, `sublet_income` ## Council tax @@ -80,7 +80,7 @@ We use the OBR's mortgage interest rate index growth rates. |-------------|------|------|------|------|------|------|------|------| | Growth rate | 26.2% | 48.5% | 22.1% | 13.6% | 12.6% | 8.2% | 4.2% | 4.7% | -We apply this to: `mortgage_interest_repayment` +We apply this to: `mortgage_interest_repayment`, `property_finance_costs`, `property_finance_costs_brought_forward` ## Private pension index diff --git a/docs/book/programs/gov/hmrc/income-tax.ipynb b/docs/book/programs/gov/hmrc/income-tax.ipynb index f523dfb995..05845e6c0e 100644 --- a/docs/book/programs/gov/hmrc/income-tax.ipynb +++ b/docs/book/programs/gov/hmrc/income-tax.ipynb @@ -448,7 +448,7 @@ " Property Allowance\n", " Property Allowance\n", " 1000.0\n", - " 2005-04-01\n", + " 2017-04-06\n", " currency-GBP\n", " \n", " \n", @@ -483,7 +483,7 @@ "15 Personal Savings Allowance Basic Threshold 1000.0 2005-04-01 \n", "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", + "18 Property Allowance Property Allowance 1000.0 2017-04-06 \n", "19 Trading Allowance Trading Allowance 1000.0 2017-04-06 \n", "\n", " Unit \n", @@ -638,8 +638,8 @@ "property_allowance_data = {\n", " \"Attribute\": [\"Property Allowance\"],\n", " \"Type\": [\"Property Allowance\"],\n", - " \"Value\": [property_allowance.values_list[-1].value],\n", - " \"Effective Date\": [property_allowance.values_list[-1].instant_str],\n", + " \"Value\": [property_allowance.values_list[0].value],\n", + " \"Effective Date\": [property_allowance.values_list[0].instant_str],\n", " \"Unit\": [property_allowance.metadata[\"unit\"]],\n", "}\n", "\n", diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index 578e74813d..cd5b2f2382 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -84,6 +84,8 @@ gov.economic_assumptions.yoy_growth.obr.petrol_spending_litre_proxy: - petrol_spending gov.economic_assumptions.yoy_growth.obr.mortgage_interest: - mortgage_interest_repayment +- property_finance_costs +- property_finance_costs_brought_forward gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - capital_gains - capital_gains_before_response @@ -110,6 +112,7 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - private_pension_wealth - private_transfer_income - property_income +- property_rental_income - savings - savings_interest_income - sublet_income diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml index 474274eadb..3da4354bd1 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml @@ -16,7 +16,7 @@ values: - working_tax_credit - state_pension - private_pension_income - - property_income + - property_income_after_finance_costs - pension_credit_deemed_income - esa_contrib - jsa_contrib diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/property_allowance.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/property_allowance.yaml index 64170ed238..c5184fa207 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/property_allowance.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/property_allowance.yaml @@ -1,8 +1,15 @@ -description: Amount of income from property untaxed per year. +description: Amount of property allowance, which relieves property 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. 783BD + href: https://www.legislation.gov.uk/ukpga/2005/5/section/783BD - 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/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml index b516d33e30..e6bc103f75 100644 --- a/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/income_tax_subtractions.yaml @@ -5,7 +5,10 @@ metadata: reference: - title: Income Tax Act 2007 s. 23 href: https://www.legislation.gov.uk/ukpga/2007/3/section/23 + - title: Income Tax Act 2007 s. 26(1)(a) + href: https://www.legislation.gov.uk/ukpga/2007/3/section/26 values: 0000-01-01: - capped_mcad - other_tax_credits + - property_finance_cost_relief diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/property_finance_costs/disallowed_share.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/property_finance_costs/disallowed_share.yaml new file mode 100644 index 0000000000..453d0b376b --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/reliefs/property_finance_costs/disallowed_share.yaml @@ -0,0 +1,36 @@ +description: Share of the costs of dwelling-related loans that cannot be deducted in calculating property business profits, and is relieved as a tax reduction instead. +values: + 2005-04-06: + value: 0 + reference: + - title: Finance (No. 2) Act 2015 s. 24 (inserts the restriction, which has effect from 2017-18) + href: https://www.legislation.gov.uk/ukpga/2015/33/section/24 + 2017-04-06: + value: 0.25 + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 272A(1) + href: https://www.legislation.gov.uk/ukpga/2005/5/section/272A + 2018-04-06: + value: 0.5 + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 272A(2) + href: https://www.legislation.gov.uk/ukpga/2005/5/section/272A + 2019-04-06: + value: 0.75 + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 272A(3) + href: https://www.legislation.gov.uk/ukpga/2005/5/section/272A + 2020-04-06: + value: 1 + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 272A(4) + href: https://www.legislation.gov.uk/ukpga/2005/5/section/272A +metadata: + label: Disallowed share of residential property finance costs + period: year + unit: /1 + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 272A + href: https://www.legislation.gov.uk/ukpga/2005/5/section/272A + - title: Finance (No. 2) Act 2015 s. 24 + href: https://www.legislation.gov.uk/ukpga/2015/33/section/24 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml index 0053eec145..d5e2aef012 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml @@ -28,6 +28,7 @@ parent: age: 30 employment_income: 20_000 + property_rental_income: 5_000 property_income: 5_000 child: age: 5 @@ -41,7 +42,8 @@ rent: 9_600 output: # Earnings take the personal allowance first: (20,000 - 12,570) x 20%. - # Property income is taxed on (5,000 - 1,000) x 20% = 800, not deducted. + # Property income (5,000 of receipts with no expenses) is taxed on + # (5,000 - 1,000) x 20% = 800, not deducted. income_tax: [2_286, 0] uc_income_tax_on_earnings: [1_486, 0] # (20,000 - 12,569.96) x 8%. @@ -59,6 +61,42 @@ # fix, gave 11,748.50: 0.55 x 800 = 440 too much. universal_credit: 11_308.50 +- name: The finance-cost reduction comes off the tax on property, not on earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + property_rental_income: 5_000 + property_income: 5_000 + property_finance_costs: 3_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + # Actual expenses beat the allowance: 5,000 x 20% - 20% x 3,000 = 400 of + # tax on property, against 800. The 600 reduction (ITTOIA 2005 s. 274AA) + # is limited by the property profits, so it does not come off the tax on + # earnings, which stays (20,000 - 12,570) x 20%. + property_finance_cost_relief: [600, 0] + income_tax: [1_886, 0] + uc_income_tax_on_earnings: [1_486, 0] + uc_individual_earned_income: [17_919.60, 0] + uc_earned_income: 12_795.60 + uc_maximum_amount: 18_346.08 + # Unchanged by the finance costs. Taking the 600 reduction off the tax on + # earnings would give 10,978.50: 0.55 x 600 = 330 less. + universal_credit: 11_308.50 + - name: Mixed-age couple, the pensioner's tax on State Pension does not reduce the partner's earnings period: 2026 absolute_error_margin: 0.01 @@ -247,9 +285,11 @@ input: age: 35 employment_income: 20_000 + property_rental_income: 5_000 property_income: 5_000 output: - # Property: (5,000 - 1,000) x 22% = 880. + # Property: 5,000 of receipts with no expenses, so (5,000 - 1,000) x 22% + # = 880. property_income_tax: 880 income_tax: 2_366 uc_income_tax_on_earnings: 1_486 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances.yaml index 5acbec23db..6f2f1f0f16 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 @@ -73,11 +73,20 @@ output: property_allowance_deduction: 100 taxable_property_income: 0 -- name: Property allowance partial deduction +- name: Property allowance does not stack on a profit already net of expenses period: 2020 absolute_error_margin: 0 input: property_income: 1001 + output: + property_allowance_deduction: 0 + taxable_property_income: 1001 +- name: Property allowance partial deduction with receipts and no expenses + period: 2020 + absolute_error_margin: 0 + input: + property_rental_income: 1001 + property_income: 1001 output: property_allowance_deduction: 1000 taxable_property_income: 1 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/is_allowance_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/is_allowance_eligible.yaml index 9fba0ca2ac..9d8e883419 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/is_allowance_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/is_allowance_eligible.yaml @@ -47,6 +47,7 @@ - name: Property Allowance Deduction period: 2024 input: + property_rental_income: 3000 property_income: 3000 gov.hmrc.income_tax.allowances.property_allowance: 1000 output: diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/property_allowance_deduction.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/property_allowance_deduction.yaml new file mode 100644 index 0000000000..e7e7daebd7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/property_allowance_deduction.yaml @@ -0,0 +1,125 @@ +# Property allowance, ITTOIA 2005 Part 6A Chapter 2 (ss. 783B-783BQ). +# property_income is profit after actual expenses. The allowance is an +# alternative to those expenses (s. 783BH(3)), so it may only add the part of +# the £1,000 that actual expenses have not already used. Receipts within the +# allowance are fully relieved (s. 783BF). + +- name: Landlord with £25,000 profit gets no allowance on top of expenses + period: 2027 + absolute_error_margin: 0.01 + input: + property_income: 25_000 + output: + property_allowance_deduction: 0 + taxable_property_income: 25_000 + adjusted_net_income: 25_000 + # (25,000 - 12,570) * 22% (property basic rate from 2027-28) + income_tax: 2_734.60 + +- name: Profit within the allowance, receipts unknown, is fully relieved + period: 2027 + absolute_error_margin: 0 + input: + property_income: 600 + output: + uses_property_allowance: true + property_allowance_deduction: 600 + taxable_property_income: 0 + +- name: Profit equal to the allowance, receipts unknown, is fully relieved + period: 2027 + absolute_error_margin: 0 + input: + property_income: 1_000 + output: + property_allowance_deduction: 1_000 + taxable_property_income: 0 + +- name: Profit above the allowance, receipts unknown, is taxed as reported + period: 2027 + absolute_error_margin: 0 + input: + property_income: 1_001 + output: + uses_property_allowance: false + property_allowance_deduction: 0 + taxable_property_income: 1_001 + +- name: Receipts of £900 are fully relieved (s. 783BF) + period: 2025 + absolute_error_margin: 0 + input: + employment_income: 30_000 + property_rental_income: 900 + property_income: 900 + output: + property_allowable_expenses: 0 + property_allowance_deduction: 900 + taxable_property_income: 0 + property_income_tax: 0 + +- name: Partial relief beats £600 of expenses on £5,000 of receipts (s. 783BH) + period: 2025 + absolute_error_margin: 0 + input: + employment_income: 30_000 + property_rental_income: 5_000 + property_income: 4_400 + output: + property_allowable_expenses: 600 + uses_property_allowance: true + property_allowance_deduction: 400 + # 5,000 receipts - 1,000 allowance + taxable_property_income: 4_000 + property_income_tax: 800 + property_finance_cost_relief: 0 + +- name: Actual expenses above the allowance leave no extra relief + period: 2025 + absolute_error_margin: 0 + input: + property_rental_income: 25_000 + property_income: 20_000 + output: + property_allowable_expenses: 5_000 + property_allowance_deduction: 0 + taxable_property_income: 20_000 + +- name: Receipts below profit are inconsistent and treated as unknown + period: 2025 + absolute_error_margin: 0 + input: + property_rental_income: 500 + property_income: 1_500 + output: + property_allowable_expenses: 0 + property_allowance_deduction: 0 + taxable_property_income: 1_500 + +- name: A property loss gets no allowance and is not taxed + period: 2025 + absolute_error_margin: 0 + input: + property_rental_income: 800 + property_income: -400 + output: + property_allowance_deduction: 0 + taxable_property_income: 0 + +- name: No property allowance before 2017-18 + period: 2016 + absolute_error_margin: 0 + input: + property_income: 600 + output: + property_allowance_deduction: 0 + taxable_property_income: 600 + +- name: The allowance applies from 2017-18 + period: 2017 + absolute_error_margin: 0 + input: + property_income: 600 + output: + property_allowance_deduction: 600 + taxable_property_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_property_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_property_income.yaml index 23522c8d12..40abf8a8c9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_property_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/bases/taxable_property_income.yaml @@ -17,18 +17,20 @@ - name: Property income partially taxable period: 2024 input: + property_rental_income: 10000 property_income: 10000 gov.hmrc.income_tax.allowances.property_allowance: 1000 output: taxable_property_income: 9000 -- name: High property income with standard allowance +- name: High property income with standard allowance, receipts unknown period: 2024 input: property_income: 50000 gov.hmrc.income_tax.allowances.property_allowance: 1000 output: - taxable_property_income: 49000 + # The profit is already net of expenses, so the allowance does not stack. + taxable_property_income: 50000 - name: Property income with no allowance period: 2024 @@ -57,6 +59,7 @@ - name: High values for both income and allowance period: 2024 input: + property_rental_income: 100000 property_income: 100000 gov.hmrc.income_tax.allowances.property_allowance: 50000 output: @@ -65,6 +68,7 @@ - name: Decimal values period: 2024 input: + property_rental_income: 1234.56 property_income: 1234.56 gov.hmrc.income_tax.allowances.property_allowance: 1000.00 output: diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.yaml new file mode 100644 index 0000000000..4cae4c3dda --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.yaml @@ -0,0 +1,360 @@ +# Residential finance costs, ITTOIA 2005 ss. 272A, 274A and 274AA. +# Costs of dwelling-related loans are not deducted from property profits +# (s. 272A(4), from 2020-21). The landlord gets a tax reduction instead: the +# property basic rate (the basic rate before 2027-28, Finance Act 2026 Sch. 1 +# para. 40) times the lowest of the relievable amount, the property profits +# and adjusted total income (s. 274AA). Unrelieved costs are carried forward +# (s. 274AA(4)). The reduction and the property allowance cannot be combined +# (s. 783BL). + +- name: £20,000 receipts, £6,000 expenses and £8,000 finance costs in 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_rental_income: 20_000 + property_income: 14_000 + property_finance_costs: 8_000 + output: + uses_property_allowance: false + taxable_property_income: 14_000 + property_finance_costs_relievable: 8_000 + property_finance_costs_relieved: 8_000 + # 20% x 8,000 + property_finance_cost_relief: 1_600 + property_finance_costs_carried_forward: 0 + # (30,000 - 12,570) x 20% + 14,000 x 20% - 1,600 + income_tax: 4_686 + +- name: The same landlord in 2027-28 gets relief at the 22% property basic rate + period: 2027 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_rental_income: 20_000 + property_income: 14_000 + property_finance_costs: 8_000 + output: + # 22% x 8,000 + property_finance_cost_relief: 1_760 + # (30,000 - 12,570) x 20% + 14,000 x 22% - 1,760 + income_tax: 4_806 + +- name: A Scottish taxpayer gets relief at the basic rate of 20% in 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + region: SCOTLAND + output: + pays_scottish_income_tax: true + property_finance_cost_relief: 1_600 + +- name: A Scottish taxpayer also gets the 22% property basic rate in 2027-28 + period: 2027 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + region: SCOTLAND + output: + pays_scottish_income_tax: true + # s. 274AA(5) gives no Scottish rate: 22% x 8,000, although the property + # profits themselves are taxed at Scottish rates. + property_finance_cost_relief: 1_760 + +- name: Relief is limited to the property profits and the rest carried forward + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 3_000 + property_finance_costs: 8_000 + output: + property_finance_costs_relieved: 3_000 + # 20% x 3,000 + property_finance_cost_relief: 600 + property_finance_costs_carried_forward: 5_000 + +- name: Relief is limited to adjusted total income + period: 2025 + absolute_error_margin: 0.01 + input: + property_income: 14_000 + property_finance_costs: 8_000 + output: + # Adjusted total income: 14,000 - 12,570 personal allowance. + property_finance_costs_relieved: 1_430 + property_finance_cost_relief: 286 + property_income_tax: 286 + income_tax: 0 + property_finance_costs_carried_forward: 6_570 + +- name: Gift Aid does not reduce adjusted total income + period: 2026 + absolute_error_margin: 0.01 + input: + property_income: 18_000 + property_finance_costs: 8_000 + gift_aid: 3_000 + output: + # Adjusted total income is net income less the personal allowance: + # 18,000 - 12,570. Gift Aid extends the basic rate band; it is not + # deducted at Step 2 or Step 3. + property_finance_costs_relieved: 5_430 + property_finance_costs_carried_forward: 2_570 + # 20% x 5,430 = 1,086, limited to the Step 5 tax, which the model works + # out with Gift Aid deducted: (18,000 - 12,570 - 3,000) x 20% = 486. + property_finance_cost_relief: 486 + income_tax: 0 + +- name: A landlord within the personal allowance gets no relief and keeps the costs + period: 2025 + absolute_error_margin: 0.01 + input: + property_income: 10_000 + property_finance_costs: 8_000 + output: + property_finance_costs_relieved: 0 + property_finance_cost_relief: 0 + income_tax: 0 + property_finance_costs_carried_forward: 8_000 + +- name: Costs brought forward are relieved with this year's costs + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 10_000 + property_finance_costs: 2_000 + property_finance_costs_brought_forward: 3_000 + output: + property_finance_costs_relievable: 5_000 + property_finance_costs_relieved: 5_000 + property_finance_cost_relief: 1_000 + property_finance_costs_carried_forward: 0 + # (30,000 - 12,570) x 20% + 10,000 x 20% - 1,000 + income_tax: 4_486 + +- name: Relief is limited to the Step 5 tax left after other reductions + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + other_tax_credits: 5_000 + output: + # Step 5 tax 3,486 + 2,800 = 6,286, less 5,000 of other reductions. + property_finance_cost_relief: 1_286 + income_tax: 0 + # The carry-forward follows the costs relieved, not the tax limit. + property_finance_costs_carried_forward: 0 + +- name: A higher-rate landlord with small finance costs elects for the allowance + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 60_000 + property_rental_income: 5_000 + property_income: 4_800 + property_finance_costs: 500 + output: + # Allowance: (5,000 - 1,000) x 40% = 1,600 on property. Expenses: + # 4,800 x 40% - 20% x 500 = 1,820. + uses_property_allowance: true + property_allowance_deduction: 800 + taxable_property_income: 4_000 + property_finance_cost_relief: 0 + # This year's costs are among the expenses the allowance replaces. + property_finance_costs_carried_forward: 0 + # 37,700 x 20% + 9,730 x 40% + 1,600 + income_tax: 13_032 + +- name: Costs brought forward stay carried forward when the allowance is used + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 60_000 + property_rental_income: 5_000 + property_income: 4_800 + property_finance_costs: 500 + property_finance_costs_brought_forward: 1_000 + output: + # Expenses: 4,800 x 40% - 20% x 1,500 = 1,620, above the allowance's 1,600. + uses_property_allowance: true + property_finance_cost_relief: 0 + property_finance_costs_carried_forward: 1_000 + +- name: A basic-rate landlord with larger finance costs keeps actual expenses + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_rental_income: 5_000 + property_income: 4_800 + property_finance_costs: 3_000 + output: + # Allowance: 4,000 x 20% = 800. Expenses: 4,800 x 20% - 20% x 3,000 = 360. + uses_property_allowance: false + property_allowance_deduction: 0 + taxable_property_income: 4_800 + property_finance_cost_relief: 600 + +- name: Receipts within the allowance keep full relief when electing out saves nothing + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_rental_income: 900 + property_income: 900 + property_finance_costs: 2_000 + output: + # Full relief: no tax on the 900. Electing out: 900 x 20% - 20% x 900 = 0. + uses_property_allowance: true + taxable_property_income: 0 + property_finance_cost_relief: 0 + property_finance_costs_carried_forward: 0 + +- name: A Scottish starter-rate landlord elects out of full relief + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 13_000 + property_rental_income: 900 + property_income: 900 + property_finance_costs: 2_000 + region: SCOTLAND + output: + # Electing out (s. 783BJ): 900 taxed at the 19% starter rate (171) and a + # 20% reduction on 900 (180), against no tax on it with full relief. + uses_property_allowance: false + taxable_property_income: 900 + property_income_tax: 171 + property_finance_cost_relief: 180 + # (13,000 - 12,570) x 19% + 171 - 180 + income_tax: 72.70 + property_finance_costs_carried_forward: 1_100 + # The 9 of the reduction above the tax on the property comes off the tax + # on earnings, as it does in income tax: 81.70 - 9. + uc_income_tax_on_earnings: 72.70 + +- name: Receipts of exactly the allowance keep full relief on a tie + period: 2025 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_rental_income: 1_000 + property_income: 1_000 + property_finance_costs: 2_000 + output: + # Electing out: 1,000 x 20% - 20% x 1,000 = 0, the same as full relief. + property_receipts_within_allowance: true + uses_property_allowance: true + taxable_property_income: 0 + property_finance_cost_relief: 0 + +- name: Half of the finance costs are deductible in 2018-19 + period: 2018 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + output: + deductible_property_finance_costs: 4_000 + taxable_property_income: 10_000 + property_finance_costs_relievable: 4_000 + property_finance_cost_relief: 800 + # (30,000 - 11,850) x 20% + 10,000 x 20% - 800 + income_tax: 4_830 + +- name: Without receipts, a profit above the allowance before 2020-21 gets no full relief + period: 2018 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 1_500 + property_finance_costs: 1_000 + output: + # Receipts are at least the 1,500 of profit before finance costs, so + # above the allowance, although the profit after the 500 still + # deductible is 1,000. + property_receipts_within_allowance: false + uses_property_allowance: false + taxable_property_income: 1_000 + # 20% x min(500 disallowed, 1,000 profit). + property_finance_cost_relief: 100 + # (30,000 - 11,850) x 20% + 1,000 x 20% - 100 + income_tax: 3_730 + +- name: Finance costs are fully deductible before 2017-18 + period: 2016 + absolute_error_margin: 0.01 + input: + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + output: + deductible_property_finance_costs: 8_000 + taxable_property_income: 6_000 + property_finance_costs_relievable: 0 + property_finance_cost_relief: 0 + +- name: Restoring full deductibility removes the reduction + period: 2026 + absolute_error_margin: 0.01 + input: + gov.hmrc.income_tax.reliefs.property_finance_costs.disallowed_share: 0 + employment_income: 30_000 + property_income: 14_000 + property_finance_costs: 8_000 + output: + deductible_property_finance_costs: 8_000 + taxable_property_income: 6_000 + property_finance_cost_relief: 0 + # (30,000 - 12,570) x 20% + 6,000 x 20% + income_tax: 4_686 + +- name: Each partner chooses with the other's route held fixed + period: 2025 + absolute_error_margin: 0.01 + input: + people: + landlord: + age: 40 + employment_income: 30_000 + property_rental_income: 5_000 + property_income: 4_800 + property_finance_costs: 850 + partner: + age: 40 + employment_income: 10_500 + property_rental_income: 900 + property_income: 900 + property_finance_costs: 1_000 + benunits: + benunit: + members: [landlord, partner] + is_married: true + households: + household: + members: [landlord, partner] + output: + # The partner keeps full relief: electing out leaves their tax at nil + # either way (11,400 is within the personal allowance). With it, they + # have 12,570 - 10,500 = 2,070 of allowance unused and transfer the + # maximum 1,260 of Marriage Allowance. + uses_property_allowance: [false, true] + marriage_allowance: [1_260, 0] + # The landlord's own choice, with the partner's held fixed: actual + # expenses give 4,800 x 20% - 20% x 850 = 790 of tax on property against + # (5,000 - 1,000) x 20% = 800 with the allowance. Switching both at once + # would also cut the transfer to 1,170 and make the allowance look + # better. + property_finance_cost_relief: [170, 0] + # (30,000 - 12,570 - 1,260) x 20% + 4,800 x 20% - 170 + income_tax: [4_024, 0] diff --git a/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml new file mode 100644 index 0000000000..902f7720a1 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml @@ -0,0 +1,79 @@ +# Means tests count property income after the costs of dwelling-related +# loans. Tax credits do so by statute: The Tax Credits (Definition and +# Calculation of Income) Regulations 2002 reg. 11(2A) disregards the ITTOIA +# 2005 s. 272A restriction, and reg. 11(1) counts "annual taxable profits", so +# costs above the profit leave nil rather than a loss. The legacy means tests +# deduct income tax, which the finance-cost reduction lowers, so they count +# the profit after the costs too. + +- name: Finance costs are deducted from property income for tax credits + period: 2023 + absolute_error_margin: 0.01 + input: + property_income: 14_000 + property_finance_costs: 8_000 + output: + property_income_after_finance_costs: 6_000 + # 6,000 less the 300 disregard on non-earned income. + tax_credits_applicable_income: 5_700 + +- name: Finance costs above the profit leave nil + period: 2023 + absolute_error_margin: 0.01 + input: + property_income: 3_000 + property_finance_costs: 8_000 + output: + property_income_after_finance_costs: 0 + +- name: Without finance costs property income is unchanged + period: 2023 + absolute_error_margin: 0.01 + input: + property_income: 3_000 + output: + property_income_after_finance_costs: 3_000 + +- name: A property loss is unchanged + period: 2023 + absolute_error_margin: 0.01 + input: + property_income: -500 + property_finance_costs: 1_000 + output: + property_income_after_finance_costs: -500 + +- name: Housing Benefit, no finance costs + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + employment_income: 16_000 + state_pension: 12_000 + property_income: 6_000 + tenure_type: RENT_FROM_COUNCIL + rent: 12_000 + output: + # (16,000 + 12,000 + 6,000 - 12,570) x 20%. + income_tax: 4_286 + # 16,000 + 12,000 + 6,000 - 4,286, less the 5 a week earnings disregard. + housing_benefit_applicable_income: 29_454 + +- name: Housing Benefit, finance costs equal to the profit do not lower it + period: 2026 + absolute_error_margin: 0.01 + input: + age: 70 + employment_income: 16_000 + state_pension: 12_000 + property_income: 6_000 + property_finance_costs: 6_000 + tenure_type: RENT_FROM_COUNCIL + rent: 12_000 + output: + # 4,286 less the 20% x 6,000 reduction. + income_tax: 3_086 + property_income_after_finance_costs: 0 + # 16,000 + 12,000 - 3,086 - 260: the property income nets to nil, so the + # lower tax does not raise the income counted. + housing_benefit_applicable_income: 24_654 diff --git a/policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml b/policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml index 067065fd1d..31a39110fa 100644 --- a/policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml +++ b/policyengine_uk/tests/policy/reforms/nov_2025_budget/income_source_tax_rates.yaml @@ -136,6 +136,7 @@ absolute_error_margin: 1 input: employment_income: 12570 + property_rental_income: 2000 property_income: 2000 output: # Property allowance is £1000, so £1000 taxable at 20% @@ -146,6 +147,7 @@ absolute_error_margin: 1 input: employment_income: 60000 + property_rental_income: 2000 property_income: 2000 output: # £1000 (after property allowance) all at 40% since employment income @@ -158,6 +160,7 @@ absolute_error_margin: 1 input: employment_income: 12570 + property_rental_income: 2000 property_income: 2000 output: # £1000 at NEW rate 22% @@ -168,6 +171,7 @@ absolute_error_margin: 1 input: employment_income: 60000 + property_rental_income: 2000 property_income: 2000 output: # £1000 at NEW rate 42% @@ -178,6 +182,7 @@ absolute_error_margin: 1 input: employment_income: 200000 + property_rental_income: 2000 property_income: 2000 output: # £1000 at NEW rate 47% @@ -194,6 +199,7 @@ input: # Person with just personal allowance worth of employment income employment_income: 12570 + property_rental_income: 2000 property_income: 2000 output: # Property allowance is £1000, so £1000 taxable at 22% @@ -207,6 +213,7 @@ absolute_error_margin: 1 input: employment_income: 60000 + property_rental_income: 2000 property_income: 2000 output: # £1000 at 42% diff --git a/policyengine_uk/tests/test_property_income_properties.py b/policyengine_uk/tests/test_property_income_properties.py new file mode 100644 index 0000000000..b4b1a59d61 --- /dev/null +++ b/policyengine_uk/tests/test_property_income_properties.py @@ -0,0 +1,363 @@ +"""Property tests for landlords' property income (ITTOIA 2005 Part 3 Chapter 3 +and Part 6A Chapter 2). + +property_income is profit after actual expenses and before the costs of +dwelling-related loans. Those costs are relieved by a tax reduction at the +property basic rate (ss. 274A-274AA), and the property allowance replaces +actual expenses but cannot be had with the reduction (s. 783BL). These tests +check bounds, the reduction's accounting, that the allowance never stacks on +expenses, that the model's choice between the two is never worse than either +alone, 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 + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEAR = 2027 +ALLOWANCE = 1_000 +PROPERTY_BASIC_RATE = 0.22 +OTHER_EMPLOYMENT_INCOME = [0, 8_000, 13_000, 30_000, 60_000, 110_000, 200_000] +# Every third person is a Scottish taxpayer. +SCOTTISH = [i % 3 == 2 for i in range(7)] +CASES = 21 # Each case is simulated twice: as drawn, and with more costs. +PEOPLE = 2 * CASES + +amount = st.one_of( + st.floats(min_value=0, max_value=2_500), + st.floats(min_value=0, max_value=60_000), + st.sampled_from([0.0, 999.99, 1_000.0, 1_000.01, 12_570.0]), +) +case = st.tuples( + st.one_of(amount, st.floats(min_value=-3_000, max_value=0)), # profit + amount, # expenses + st.sampled_from([0.0, 0.0, 300.0, 1_500.0]).flatmap( + lambda base: st.one_of(st.just(base), amount) + ), # finance costs + st.sampled_from([0.0, 0.0, 0.0, 500.0, 4_000.0]), # brought forward + # Receipts: unknown, consistent (profit + expenses), or inconsistently + # below profit, which the model must treat as unknown. + st.sampled_from(["unknown", "consistent", "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 finance costs +) +cases = st.lists(case, min_size=CASES, max_size=CASES) + + +def person_index(i): + return i % CASES % len(OTHER_EMPLOYMENT_INCOME) + + +@pytest.fixture(scope="module") +def sim(): + people = { + f"person_{i}": { + "age": {YEAR: 40}, + "employment_income": {YEAR: OTHER_EMPLOYMENT_INCOME[person_index(i)]}, + "property_income": {YEAR: 0}, + "property_rental_income": {YEAR: 0}, + "property_finance_costs": {YEAR: 0}, + "property_finance_costs_brought_forward": {YEAR: 0}, + } + for i in range(PEOPLE) + } + households = { + f"household_{i}": { + "members": [f"person_{i}"], + "region": {YEAR: "SCOTLAND" if SCOTTISH[person_index(i)] else "LONDON"}, + } + for i in range(PEOPLE) + } + return Simulation( + situation={ + "people": people, + "benunits": { + f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(PEOPLE) + }, + "households": households, + } + ) + + +def set_inputs(sim, drawn, uses_allowance=None): + profit, expenses, costs, brought_forward, mode, share, extra = map( + np.array, zip(*drawn) + ) + profit, expenses, brought_forward, mode, share = ( + np.concatenate([x, x]) for x in (profit, expenses, brought_forward, mode, share) + ) + costs = np.concatenate([costs, costs + extra]) + receipts = np.select( + [mode == "consistent", mode == "below_profit"], + [np.maximum(profit, 0) + expenses, np.maximum(profit, 0) * share], + 0, + ) + sim.set_input("property_income", YEAR, profit) + sim.set_input("property_rental_income", YEAR, receipts) + sim.set_input("property_finance_costs", YEAR, costs) + sim.set_input("property_finance_costs_brought_forward", YEAR, brought_forward) + sim.reset_calculations() + if uses_allowance is not None: + sim.set_input("uses_property_allowance", YEAR, np.full(PEOPLE, uses_allowance)) + + +def run(sim, drawn): + set_inputs(sim, drawn) + + def calc(variable): + return sim.calculate(variable, YEAR).astype(float) + + # Compare against the stored (float32) inputs, not the float64 draws. + profit = calc("property_income") + receipts = calc("property_rental_income") + return dict( + profit=profit, + receipts=receipts, + known=(receipts > 0) & (receipts >= profit), + expenses=calc("property_allowable_expenses"), + costs=calc("property_finance_costs"), + brought_forward=calc("property_finance_costs_brought_forward"), + uses=sim.calculate("uses_property_allowance", YEAR), + deduction=calc("property_allowance_deduction"), + deduction_if_used=calc("property_allowance_deduction_if_used"), + taxable=calc("taxable_property_income"), + relievable=calc("property_finance_costs_relievable"), + relieved=calc("property_finance_costs_relieved"), + relief=calc("property_finance_cost_relief"), + carried_forward=calc("property_finance_costs_carried_forward"), + pre_charges=calc("income_tax_pre_charges"), + adjusted_net_income=calc("adjusted_net_income"), + personal_allowance=calc("personal_allowance"), + income_tax=calc("income_tax"), + ) + + +def income_tax_on_route(sim, drawn, uses_allowance): + set_inputs(sim, drawn, uses_allowance) + return sim.calculate("income_tax", YEAR).astype(float) + + +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=30, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +@SETTINGS +@given(cases) +def test_allowance_is_bounded_and_never_stacks_on_expenses(sim, drawn): + r = run(sim, drawn) + assert (r["deduction"] >= 0).all() + assert (r["deduction"] <= ALLOWANCE + tolerance(ALLOWANCE)).all() + assert (r["deduction"] <= np.maximum(r["profit"], 0) + tolerance(r["profit"])).all() + # Deducted from receipts: never more than the larger of actual expenses + # and the allowance. + known = r["known"] & (r["receipts"] > ALLOWANCE) + deducted = r["receipts"] - r["taxable"] + limit = np.maximum(r["expenses"], ALLOWANCE) + tolerance(r["receipts"]) + assert (deducted[known] <= limit[known]).all() + # Receipts within the allowance are relieved in full unless the person + # elects out of it. + full = r["known"] & (r["receipts"] <= ALLOWANCE) & r["uses"] + assert (r["taxable"][full] <= tolerance(r["receipts"])[full]).all() + + +@SETTINGS +@given(cases) +def test_taxable_profit_matches_statutory_reference(sim, drawn): + r = run(sim, drawn) + profit = np.maximum(r["profit"], 0) + allowance_route = np.where( + r["known"], + np.where( + r["receipts"] <= ALLOWANCE, + 0, + np.minimum(profit, r["receipts"] - ALLOWANCE), + ), + np.where(r["profit"] <= ALLOWANCE, 0, profit), + ) + expected = np.where(r["uses"], allowance_route, profit) + assert ( + np.abs(r["taxable"] - expected) <= tolerance(r["receipts"], r["profit"]) + ).all() + + +@SETTINGS +@given(cases) +def test_relief_accounting(sim, drawn): + r = run(sim, drawn) + expenses_route = ~r["uses"] + # Relievable: this year's disallowed costs plus the amount brought forward. + assert ( + np.abs(r["relievable"] - (r["costs"] + r["brought_forward"])) + <= tolerance(r["costs"], r["brought_forward"]) + ).all() + # Relieved: the lowest of the relievable amount, the property profits and + # adjusted total income, on the expenses route only. The generated people + # have no savings, dividends, Step 2 reliefs or blind person's allowance, + # so adjusted total income is net income less the personal allowance. + adjusted_total_income = np.maximum( + 0, r["adjusted_net_income"] - r["personal_allowance"] + ) + expected_relieved = np.where( + expenses_route, + np.maximum( + 0, + np.minimum( + r["relievable"], + np.minimum(r["taxable"], adjusted_total_income), + ), + ), + 0, + ) + assert ( + np.abs(r["relieved"] - expected_relieved) + <= tolerance(r["relievable"], r["taxable"], adjusted_total_income) + ).all() + # The reduction is at most the property basic rate on the costs, and at + # most the Step 5 tax. + assert (r["relief"] >= 0).all() + assert ( + r["relief"] + <= PROPERTY_BASIC_RATE * r["relievable"] + tolerance(r["relievable"]) + ).all() + assert (r["relief"] <= r["pre_charges"] + tolerance(r["pre_charges"])).all() + # What is not relieved is carried forward; on the allowance route this + # year's costs are replaced by the allowance and only the amount brought + # forward is carried on. + expected_carried = np.where( + expenses_route, r["relievable"] - r["relieved"], r["brought_forward"] + ) + assert ( + np.abs(r["carried_forward"] - expected_carried) + <= tolerance(r["relievable"], r["relieved"]) + ).all() + + +@SETTINGS +@given(cases) +def test_choice_is_never_worse_than_either_route(sim, drawn): + r = run(sim, drawn) + with_allowance = income_tax_on_route(sim, drawn, True) + with_expenses = income_tax_on_route(sim, drawn, False) + margin = tolerance(r["income_tax"], with_allowance, with_expenses) + assert (r["income_tax"] <= with_expenses + margin).all() + # The allowance route exists where the allowance deducts something. + available = r["deduction_if_used"] > 0 + assert (r["income_tax"][available] <= (with_allowance + margin)[available]).all() + + +@SETTINGS +@given(cases) +def test_income_tax_is_non_increasing_in_finance_costs(sim, drawn): + r = run(sim, drawn) + base, more = r["income_tax"][:CASES], r["income_tax"][CASES:] + assert (more <= base + tolerance(base)).all() + + +# Couples: a partner's route can move a person's tax through the Marriage +# Allowance, so each person must choose with the other's route held fixed. +COUPLES = 12 +COUPLE_EMPLOYMENT_INCOME = [0, 9_000, 11_000, 20_000, 30_000, 45_000, 60_000] +couple_member = st.tuples( + st.sampled_from(COUPLE_EMPLOYMENT_INCOME), + st.one_of(amount, st.floats(min_value=-1_000, max_value=0)), # profit + amount, # expenses + st.sampled_from([0.0, 300.0, 850.0, 1_500.0, 6_000.0]), # finance costs + st.sampled_from(["unknown", "consistent", "consistent"]), +) +# A close call: the allowance's value over actual expenses (1,000 - expenses) +# within 150 of the finance costs it would give up. +close_call = st.tuples( + st.sampled_from([20_000, 30_000, 45_000]), + st.floats(min_value=1_500, max_value=20_000), # profit + st.floats(min_value=0, max_value=950), # expenses + st.floats(min_value=-150, max_value=150), # finance costs less the gap +).map(lambda m: (m[0], m[1], m[2], max(1_000 - m[2] + m[3], 1.0), "consistent")) +# A partner within the personal allowance with receipts within the property +# allowance and finance costs, whose route moves their unused allowance. +transferor = st.tuples( + st.floats(min_value=8_000, max_value=12_500), # employment income + st.floats(min_value=50, max_value=1_000), # profit = receipts + st.floats(min_value=50, max_value=3_000), # finance costs +).map(lambda m: (m[0], m[1], 0.0, m[2], "consistent")) +couples = st.lists( + st.one_of( + st.tuples(couple_member, couple_member), + st.tuples(close_call, transferor), + ), + min_size=COUPLES, + max_size=COUPLES, +) + + +@pytest.fixture(scope="module") +def couple_sim(): + people, benunits, households = {}, {}, {} + for c in range(COUPLES): + members = [] + for k in range(2): + name = f"person_{c}_{k}" + members.append(name) + people[name] = { + "age": {YEAR: 40}, + "employment_income": {YEAR: 0}, + "property_income": {YEAR: 0}, + "property_rental_income": {YEAR: 0}, + "property_finance_costs": {YEAR: 0}, + } + benunits[f"benunit_{c}"] = {"members": members, "is_married": {YEAR: True}} + households[f"household_{c}"] = {"members": members} + return Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + + +def set_couple_inputs(sim, drawn, uses_allowance=None): + members = [member for couple in drawn for member in couple] + employment, profit, expenses, costs, mode = map(np.array, zip(*members)) + receipts = np.where(mode == "consistent", np.maximum(profit, 0) + expenses, 0) + # The simulation moves employment_income to employment_income_before_lsr + # when it is built, so that is the input to set. + sim.set_input("employment_income_before_lsr", YEAR, employment.astype(float)) + sim.set_input("property_income", YEAR, profit) + sim.set_input("property_rental_income", YEAR, receipts) + sim.set_input("property_finance_costs", YEAR, costs) + sim.reset_calculations() + if uses_allowance is not None: + sim.set_input("uses_property_allowance", YEAR, uses_allowance) + + +@SETTINGS +@given(couples) +def test_no_one_lowers_their_tax_by_switching_route_alone(couple_sim, drawn): + set_couple_inputs(couple_sim, drawn) + routes = couple_sim.calculate("uses_property_allowance", YEAR) + income_tax = couple_sim.calculate("income_tax", YEAR).astype(float) + profit = couple_sim.calculate("property_income", YEAR).astype(float) + choosing = ( + (couple_sim.calculate("property_allowance_deduction_if_used", YEAR) > 0) + & (couple_sim.calculate("property_finance_costs_relievable", YEAR) > 0) + & (profit > 0) + ) + position = np.arange(2 * COUPLES) % 2 + for k in range(2): + switching = choosing & (position == k) + if not switching.any(): + continue + set_couple_inputs(couple_sim, drawn, routes ^ switching) + switched_tax = couple_sim.calculate("income_tax", YEAR).astype(float) + margin = tolerance(income_tax, switched_tax) + assert (switched_tax[switching] >= (income_tax - margin)[switching]).all() diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 6119abf2e1..8807c5ed6a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -66,7 +66,7 @@ def formula(benunit, period, parameters): INCOME_COMPONENTS = [ "employment_income", "self_employment_income", - "property_income", + "property_income_after_finance_costs", "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index 17aa562d36..eea502562e 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -22,7 +22,7 @@ def formula(benunit, period, parameters): INCOME_COMPONENTS = [ "employment_income", "self_employment_income", - "property_income", + "property_income_after_finance_costs", "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py index 1c698c1921..0d3a4cc2c2 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -25,7 +25,7 @@ def formula(benunit, period, parameters): "private_pension_income", "savings_interest_income", "dividend_income", - "property_income", + "property_income_after_finance_costs", ] income = add_for_members(benunit, period, STEP_1_COMPONENTS, members) income = max_(income - TC.means_test.non_earned_disregard, 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py index 9f53b210f2..88ba53027c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py @@ -91,6 +91,24 @@ def formula(person, period, parameters): # off the tax on earnings first, as allowances do. So the deduction # never exceeds the income tax the person pays, never includes a # charge such as the High Income Child Benefit Charge, and does not - # move when other income absorbs more or less of a reduction. - reductions = add(person, period, income_tax.income_tax_subtractions) + # move when other income absorbs more or less of a reduction. The + # exception is the finance-cost reduction, which is given on property + # profits and limited by them (ITTOIA 2005 s. 274AA), so it comes off + # the tax on property income first. Only a part larger than that tax + # (possible where Scottish rates on property income are below the + # reduction's rate) comes off the tax on earnings. + reductions = add( + person, + period, + [ + variable + for variable in income_tax.income_tax_subtractions + if variable != "property_finance_cost_relief" + ], + ) + reductions += max_( + 0, + person("property_finance_cost_relief", period) + - person("property_income_tax", period), + ) return max_(0, tax - reductions) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction.py index 9ec79d5574..609965dea5 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction.py @@ -5,15 +5,37 @@ class property_allowance_deduction(Variable): value_type = float entity = Person label = "Deduction applied by the property allowance" + documentation = ( + "Relief from the property allowance beyond the expenses already " + "deducted in property_income: property_allowance_deduction_if_used " + "where the person uses the allowance, and nil where they deduct " + "actual expenses and take the finance-cost tax reduction instead " + "(uses_property_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. 783BD", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BD", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BF (full relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BF", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BH (partial relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BH", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BL", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BL", + ), + ] unit = GBP def formula(person, period, parameters): - property_allowance = parameters( - period - ).gov.hmrc.income_tax.allowances.property_allowance - return min_( - person("property_income", period), - property_allowance, + return where( + person("uses_property_allowance", period), + person("property_allowance_deduction_if_used", period), + 0, ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction_if_used.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction_if_used.py new file mode 100644 index 0000000000..bc6c3d2f6d --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_allowance_deduction_if_used.py @@ -0,0 +1,67 @@ +from policyengine_uk.model_api import * + + +class property_allowance_deduction_if_used(Variable): + value_type = float + entity = Person + label = "Deduction the property allowance gives if used" + documentation = ( + "What the property allowance would deduct from property profits, " + "beyond the expenses already deducted in them, if the person used it. " + "The allowance replaces actual expenses 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 of " + "their expenses, without creating a loss. With gross receipts " + "supplied, this is the allowance less actual expenses, 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 = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BF (full relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BF", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BH (partial relief)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BH", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BI(4)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BI", + ), + ] + unit = GBP + + def formula(person, period, parameters): + allowance = parameters(period).gov.hmrc.income_tax.allowances.property_allowance + receipts = person("property_rental_income", period) + deductible_finance_costs = person("deductible_property_finance_costs", period) + # Profit as computed with actual expenses, which include any finance + # costs still deductible (before 2020-21). The allowance replaces all + # of them. + profit = person("property_income", period) - deductible_finance_costs + expenses = person("property_allowable_expenses", period) + ( + deductible_finance_costs + ) + # Receipts below profit (including the default zero) mean unknown. + receipts_known = (receipts > 0) & ( + receipts >= person("property_income", period) + ) + # Full relief (receipts within the allowance, s. 783BF) makes the + # profit nil. Partial relief (s. 783BH) replaces actual expenses with + # the allowance, which comes to the allowance's excess over them, + # capped at profit; without receipts the expenses are unknown, and a + # profit above the allowance is taken to reflect the better of the + # two already. + partial_relief = where(receipts_known, min_(profit, allowance - expenses), 0) + relief = where( + person("property_receipts_within_allowance", period), + profit, + partial_relief, + ) + return max_(relief, 0) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_receipts_within_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_receipts_within_allowance.py new file mode 100644 index 0000000000..2c6b6a3c7d --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/property_receipts_within_allowance.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class property_receipts_within_allowance(Variable): + value_type = bool + entity = Person + label = "Property receipts are within the property allowance" + documentation = ( + "Whether the person's relevant property income (gross receipts) does " + "not exceed the property allowance, so that full relief applies unless " + "the person elects out of it. Without gross receipts, a " + "property_income within the allowance is taken as coming from " + "receipts within it. property_income is measured before finance " + "costs, so receipts are never below it." + ) + definition_period = YEAR + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BE", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BE", + ) + + def formula(person, period, parameters): + allowance = parameters(period).gov.hmrc.income_tax.allowances.property_allowance + profit = person("property_income", period) + receipts = person("property_rental_income", period) + # Receipts below profit (including the default zero) mean unknown. + receipts_known = (receipts > 0) & (receipts >= profit) + return where(receipts_known, receipts <= allowance, profit <= allowance) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py new file mode 100644 index 0000000000..6e77ce8650 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py @@ -0,0 +1,133 @@ +from policyengine_uk.model_api import * + +# Inputs to income tax that do not depend on how property profits are +# computed. Resolving them before branching lets both branches reuse them. +INDEPENDENT_OF_ROUTE = [ + "taxable_employment_income", + "taxable_self_employment_income", + "taxable_pension_income", + "taxable_social_security_income", + "taxable_savings_interest_income", + "taxable_dividend_income", + "taxable_miscellaneous_income", + "pays_scottish_income_tax", + "other_tax_credits", +] +# Rounds of decisions within a benefit unit before the routes are taken as +# settled. Partners' routes interact only through small amounts (such as the +# Marriage Allowance), so a second round rarely changes anything. +MAX_ROUNDS = 3 + + +def income_tax_with_route(simulation, name, period, uses_allowance): + """Each person's income tax with the given choices of property allowance. + + The branch shares the simulation's cached arrays until it writes, and is + dropped afterwards so a later period starts from a fresh branch. + """ + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + branch.set_input("uses_property_allowance", period, uses_allowance) + return branch.populations["person"]("income_tax", period) + finally: + del simulation.branches[name] + + +class uses_property_allowance(Variable): + value_type = bool + entity = Person + label = "Uses the property allowance" + documentation = ( + "Whether the person's property profits are computed with the property " + "allowance, by full relief or an election for partial relief, rather " + "than with actual expenses and the finance-cost tax reduction. The " + "two cannot be combined. Full relief applies to receipts within the " + "allowance unless the person elects out of it, and partial relief " + "only if they elect for it; each election is made only if it lowers " + "the person's own income tax, with everyone else's route held fixed. " + "Ties keep the default, so a person with receipts within the " + "allowance keeps full relief even where electing out would carry " + "finance costs forward. Without finance costs to relieve, the " + "allowance is used whenever it lowers taxable property income." + ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BE", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BE", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BJ", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BJ", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BK", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BK", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BL", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BL", + ), + ] + + def formula(person, period, parameters): + deduction = person("property_allowance_deduction_if_used", period) + profit = person("property_income", period) - person( + "deductible_property_finance_costs", period + ) + lowers_income = deduction > 0 + # The finance-cost reduction needs property profits to relieve. + reduction_at_stake = ( + person("property_finance_costs_relievable", period) > 0 + ) & (profit > 0) + # Without a reduction at stake, the allowance is used whenever it + # lowers taxable property income. Otherwise compare income tax on + # each route. + uses = lowers_income & ~reduction_at_stake + choosing = lowers_income & reduction_at_stake + if not choosing.any(): + # Nobody has a choice to weigh: skip the branches. + return uses + # Full relief applies unless electing out of it (s. 783BJ) lowers + # tax; partial relief applies only if electing for it (s. 783BK) + # lowers tax. Until a person decides, they keep that default. + full_relief = person("property_receipts_within_allowance", period) + route = where(choosing, full_relief, uses) + for variable in INDEPENDENT_OF_ROUTE: + person(variable, period) + simulation = person.simulation + # One person per benefit unit decides at a time, with everyone + # else's route held fixed: a partner's route can move this person's + # tax (through the Marriage Allowance, for example), and comparing + # both partners' switches at once would count that. Rounds repeat + # until nobody changes. + order = person.get_rank( + person.benunit, np.zeros(person.count), condition=choosing + ) + for _ in range(MAX_ROUNDS): + changed = False + for rank in range(int(order.max()) + 1): + deciding = choosing & (order == rank) + tax_using = income_tax_with_route( + simulation, "property_allowance_used", period, route | deciding + ) + tax_not_using = income_tax_with_route( + simulation, + "property_allowance_not_used", + period, + route & ~deciding, + ) + # Tax is computed to the penny; smaller differences are + # rounding. + saving = np.round(tax_not_using - tax_using, 2) + prefers_allowance = where(full_relief, saving >= 0, saving > 0) + decided = where(deciding, prefers_allowance, route) + changed = changed or bool((decided != route).any()) + route = decided + # With one person deciding per benefit unit, everyone else's + # route was fixed throughout, so one round settles it. + if order.max() == 0 or not changed: + break + return route diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/deductible_property_finance_costs.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/deductible_property_finance_costs.py new file mode 100644 index 0000000000..608b8f935c --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/deductible_property_finance_costs.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class deductible_property_finance_costs(Variable): + value_type = float + entity = Person + label = "deductible residential property finance costs" + documentation = ( + "The part of property_finance_costs still deducted in calculating " + "property profits: all of it before 2017-18, then 75%, 50% and 25%, " + "and none from 2020-21. The rest is relieved as a tax reduction." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272A", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272A", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc.income_tax.reliefs.property_finance_costs + return (1 - p.disallowed_share) * person("property_finance_costs", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/property_allowable_expenses.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/property_allowable_expenses.py new file mode 100644 index 0000000000..f6e5345d5d --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/property_allowable_expenses.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class property_allowable_expenses(Variable): + value_type = float + entity = Person + label = "property allowable expenses" + documentation = ( + "Expenses already deducted in arriving at property_income, implied by " + "gross receipts: property_rental_income less property_income. They do " + "not include the costs of dwelling-related loans " + "(property_finance_costs), which property_income is measured before. " + "Zero when receipts are unknown, that is zero or below " + "property_income." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272", + ) + + def formula(person, period, parameters): + profit = person("property_income", period) + receipts = person("property_rental_income", period) + receipts_known = (receipts > 0) & (receipts >= profit) + return where(receipts_known, receipts - profit, 0) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_property_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_property_income.py index d70b06548b..cac53280c7 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_property_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/bases/taxable_property_income.py @@ -5,16 +5,28 @@ class taxable_property_income(Variable): value_type = float entity = Person label = "Amount of property income that is taxable" - definition_period = YEAR - reference = dict( - title="Income Tax (Trading and Other Income) Act 2005, s. 268", - href="https://www.legislation.gov.uk/ukpga/2005/5/section/268", + documentation = ( + "Property profits for income tax: property_income less any finance " + "costs still deductible (before 2020-21) and the property allowance " + "deduction, floored at nil." ) + definition_period = YEAR + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 268", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/268", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272A", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272A", + ), + ] unit = GBP def formula(person, period, parameters): return max_( 0, person("property_income", period) + - person("deductible_property_finance_costs", period) - person("property_allowance_deduction", period), ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/other_tax_credits.py b/policyengine_uk/variables/gov/hmrc/income_tax/other_tax_credits.py index c1d107d5ff..dcf7260c39 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/other_tax_credits.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/other_tax_credits.py @@ -5,7 +5,8 @@ class other_tax_credits(Variable): label = "other miscellaneous tax credits for Income Tax" documentation = """Includes Venture Capital Trusts, Enterprise Investment Schemes, Seed Enterprise Investment Schemes, Community Investment Tax Relief, maintenance/alimony payments, -Social Investment Tax Relief, foreign tax relief on income and landlord tax relief.""" +Social Investment Tax Relief and foreign tax relief on income. Landlords' tax reduction for +residential finance costs is property_finance_cost_relief, not this.""" entity = Person definition_period = YEAR value_type = float diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.py new file mode 100644 index 0000000000..b23956b772 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_cost_relief.py @@ -0,0 +1,57 @@ +from policyengine_uk.model_api import * + + +class property_finance_cost_relief(Variable): + value_type = float + entity = Person + label = "residential property finance costs tax reduction" + documentation = ( + "Tax reduction for the costs of dwelling-related loans that cannot be " + "deducted from property profits: the property basic rate (the basic " + "rate before 2027-28) times the finance costs relieved this year. " + "The rate is the same for Scottish taxpayers, whose property income " + "is taxed at Scottish rates. It is deducted at Step 6 of the income " + "tax calculation after the person's other tax reductions, and only " + "so far as there is tax left to deduct it from." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274A", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274A", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274AA(5)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", + ), + dict( + title="Finance Act 2026, Sch. 1 para. 40 (property basic rate from 2027-28)", + href="https://www.legislation.gov.uk/ukpga/2026/11/schedule/1", + ), + dict( + title="Income Tax Act 2007, s. 26(1)(a) and s. 29(2)-(3)", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/29", + ), + ] + + def formula(person, period, parameters): + income_tax = parameters(period).gov.hmrc.income_tax + reduction = income_tax.rates.property.basic * person( + "property_finance_costs_relieved", period + ) + # The order of Step 6 reductions does not change their total, so the + # person's other reductions come first; this one is limited to the + # Step 5 tax they leave. Step 7 charges (ITA 2007 s. 30), such as the + # High Income Child Benefit Charge, are not reduced. + other_reductions = [ + variable + for variable in income_tax.income_tax_subtractions + if variable != "property_finance_cost_relief" + ] + tax_left = max_( + 0, + person("income_tax_pre_charges", period) + - add(person, period, other_reductions), + ) + return min_(reduction, tax_left) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_carried_forward.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_carried_forward.py new file mode 100644 index 0000000000..ebc812bd20 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_carried_forward.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class property_finance_costs_carried_forward(Variable): + value_type = float + entity = Person + label = "residential property finance costs carried forward" + documentation = ( + "Finance costs left unrelieved this year, which become next year's " + "property_finance_costs_brought_forward: the relievable amount less " + "the costs relieved. Where the person uses the property allowance, " + "this year's costs are among the expenses the allowance replaces, so " + "only the amount brought forward is carried on." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274AA(4)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", + ) + + def formula(person, period, parameters): + relievable = where( + person("uses_property_allowance", period), + person("property_finance_costs_brought_forward", period), + person("property_finance_costs_relievable", period), + ) + return max_(0, relievable - person("property_finance_costs_relieved", period)) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relievable.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relievable.py new file mode 100644 index 0000000000..3add668734 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relievable.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class property_finance_costs_relievable(Variable): + value_type = float + entity = Person + label = "residential property finance costs available for relief" + documentation = ( + "The relievable amount for the finance-cost tax reduction: this " + "year's finance costs that cannot be deducted from property profits, " + "plus any amount brought forward from earlier years." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274A(3)-(4)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274A", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc.income_tax.reliefs.property_finance_costs + current_year = p.disallowed_share * person("property_finance_costs", period) + return current_year + person("property_finance_costs_brought_forward", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relieved.py b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relieved.py new file mode 100644 index 0000000000..1ca09de9fc --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/reliefs/property_finance_costs_relieved.py @@ -0,0 +1,59 @@ +from policyengine_uk.model_api import * + + +class property_finance_costs_relieved(Variable): + value_type = float + entity = Person + label = "residential property finance costs relieved" + documentation = ( + "The finance costs on which the tax reduction is given this year: the " + "lowest of the relievable amount, the property profits, and adjusted " + "total income (net income other than savings and dividend income, " + "less the personal and blind person's allowances). Nil where the " + "person uses the property allowance, since the allowance replaces the " + "expenses and cannot be had with the reduction." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274AA(2), (3) and (6)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BL", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BL", + ), + ] + + def formula(person, period, parameters): + # Property profits for the year; s. 118 ITA 2007 loss relief, which + # would come off them first, is not modelled. + profits = person("taxable_property_income", period) + # Adjusted total income (s. 274AA(6)): net income (after the Step 2 + # reliefs, such as gifts of shares to charity), less savings and + # dividend income, less the allowances deducted at Step 3 of ITA 2007 + # s. 23, which are the personal and blind person's allowances. Gift + # Aid, covenanted payments and pension contributions relieved at + # source extend the basic rate band instead of reducing net income, + # so they do not come off; pension_contributions_relief does not + # separate contributions under net pay arrangements, which would. + step_2_reliefs = add( + person, period, ["charitable_investment_gifts", "other_deductions"] + ) + step_3_allowances = add( + person, period, ["personal_allowance", "blind_persons_allowance"] + ) + adjusted_total_income = max_( + 0, + person("adjusted_net_income", period) + - person("taxable_savings_interest_income", period) + - person("taxable_dividend_income", period) + - step_2_reliefs + - step_3_allowances, + ) + relieved = min_( + person("property_finance_costs_relievable", period), + min_(profits, adjusted_total_income), + ) + return where(person("uses_property_allowance", period), 0, max_(relieved, 0)) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index a970b6995b..c6bf7bb2c0 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -129,7 +129,7 @@ def normal_gross_income_non_dep_deduction( gross_income_components = [ "employment_income", "self_employment_income", - "property_income", + "property_income_after_finance_costs", "private_pension_income", "savings_interest_income", "dividend_income", diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index b86cabc27d..01fdaaedb1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -77,7 +77,7 @@ def formula(benunit, period, parameters): income_components = [ "employment_income", "self_employment_income", - "property_income", + "property_income_after_finance_costs", "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income diff --git a/policyengine_uk/variables/household/income/property_income_after_finance_costs.py b/policyengine_uk/variables/household/income/property_income_after_finance_costs.py new file mode 100644 index 0000000000..1fbfc37134 --- /dev/null +++ b/policyengine_uk/variables/household/income/property_income_after_finance_costs.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class property_income_after_finance_costs(Variable): + value_type = float + entity = Person + label = "property income after finance costs" + documentation = ( + "property_income less the year's costs of dwelling-related loans " + "(property_finance_costs): the landlord's profit after mortgage " + "interest. Income tax does not deduct these costs, and gives the " + "finance-cost tax reduction instead, but the means tests that count " + "property income count it after them. Tax credits do so by statute. " + "The legacy means tests also deduct the person's income tax, which " + "the reduction lowers, so counting the profit before the costs would " + "make finance costs raise the income they count. Costs above the " + "profit leave nil, not a loss set against other income." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="The Tax Credits (Definition and Calculation of Income) Regulations 2002 reg. 11(1) and (2A)", + href="https://www.legislation.gov.uk/uksi/2002/2006/regulation/11", + ) + + def formula(person, period, parameters): + profit = person("property_income", period) + finance_costs = person("property_finance_costs", period) + return where(profit > 0, max_(profit - finance_costs, 0), profit) diff --git a/policyengine_uk/variables/input/property_finance_costs.py b/policyengine_uk/variables/input/property_finance_costs.py new file mode 100644 index 0000000000..4179c30470 --- /dev/null +++ b/policyengine_uk/variables/input/property_finance_costs.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class property_finance_costs(Variable): + value_type = float + entity = Person + label = "residential property finance costs" + documentation = ( + "The person's share of the year's costs of dwelling-related loans of " + "their property businesses: interest (and returns economically " + "equivalent to interest) on money borrowed for letting homes, such " + "as buy-to-let mortgages, and the incidental costs of obtaining that " + "finance. ITTOIA 2005 s. 272A stops them being deducted from property " + "profits (in full from 2020-21) and ss. 274A-274AA give a tax " + "reduction instead. property_income is measured before these costs." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272A", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272A", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272B", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272B", + ), + ] + quantity_type = FLOW + uprating = "gov.economic_assumptions.indices.obr.mortgage_interest" diff --git a/policyengine_uk/variables/input/property_finance_costs_brought_forward.py b/policyengine_uk/variables/input/property_finance_costs_brought_forward.py new file mode 100644 index 0000000000..847556cb41 --- /dev/null +++ b/policyengine_uk/variables/input/property_finance_costs_brought_forward.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class property_finance_costs_brought_forward(Variable): + value_type = float + entity = Person + label = "residential property finance costs brought forward" + documentation = ( + "Finance costs of earlier years that have not yet been relieved and " + "are carried forward into this year (the brought-forward amount of " + "ITTOIA 2005 s. 274AA(4)). They are relieved with this year's " + "property_finance_costs. The previous year's " + "property_finance_costs_carried_forward." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 274AA(4)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", + ) + quantity_type = FLOW + # Projected with the costs they came from. + uprating = "gov.economic_assumptions.indices.obr.mortgage_interest" diff --git a/policyengine_uk/variables/input/property_income.py b/policyengine_uk/variables/input/property_income.py index 9855400106..5ea5f90b65 100644 --- a/policyengine_uk/variables/input/property_income.py +++ b/policyengine_uk/variables/input/property_income.py @@ -4,10 +4,27 @@ class property_income(Variable): value_type = float entity = Person - label = "rental income" - documentation = "Income from rental of property" + label = "property income" + documentation = ( + "Profits of the person's UK and overseas property businesses: rents " + "and other receipts less allowable expenses, before the property " + "allowance and before any costs of dwelling-related loans (such as " + "mortgage interest on let homes), which have not been deductible " + "since 2020-21 and are relieved as a tax reduction instead " + "(property_finance_costs). This is the profit concept of the SPI's " + "net property income. Gross receipts are property_rental_income." + ) definition_period = YEAR - reference = "Income Tax (Trading and Other Income) Act 2005 s. 1(1)(b)" + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 268", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/268", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 272A", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/272A", + ), + ] unit = GBP quantity_type = FLOW uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" diff --git a/policyengine_uk/variables/input/property_rental_income.py b/policyengine_uk/variables/input/property_rental_income.py new file mode 100644 index 0000000000..8f24dbf137 --- /dev/null +++ b/policyengine_uk/variables/input/property_rental_income.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class property_rental_income(Variable): + value_type = float + entity = Person + label = "gross property rental receipts" + documentation = ( + "Gross rents and other receipts of the person's UK and overseas " + "property businesses, before any expenses. Rent-a-room receipts are " + "not included. Optional: used only to apply the property allowance, " + "which is measured against gross receipts and replaces actual " + "expenses. It does not replace property_income, the profit that is " + "taxed: set both. Must be at least property_income; leave at zero " + "when unknown, and receipts below property_income are treated as " + "unknown." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BB", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BB", + ), + dict( + title="Income Tax (Trading and Other Income) Act 2005, s. 783BC", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BC", + ), + ] + quantity_type = FLOW + # The same index as property_income, so that uprated receipts never fall + # below uprated profit and the implied expenses keep their share. + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp"