diff --git a/changelog.d/boarder-lodger-rent.added.md b/changelog.d/boarder-lodger-rent.added.md new file mode 100644 index 000000000..10b057539 --- /dev/null +++ b/changelog.d/boarder-lodger-rent.added.md @@ -0,0 +1 @@ +Model rent from boarders, lodgers and sub-tenants for the householder: rent-a-room relief in income tax, the £20 and board-and-lodging disregards in the legacy means tests (not Universal Credit), and no non-dependant deductions for people who pay the householder rent. diff --git a/policyengine_uk/data/uprating_indices.yaml b/policyengine_uk/data/uprating_indices.yaml index fe3072c0c..2019fa3a5 100644 --- a/policyengine_uk/data/uprating_indices.yaml +++ b/policyengine_uk/data/uprating_indices.yaml @@ -103,6 +103,8 @@ gov.economic_assumptions.yoy_growth.obr.per_capita.gdp: - private_pension_wealth - private_transfer_income - property_income +- rent_paid_as_boarder +- rent_paid_as_lodger - savings - savings_interest_income - sublet_income diff --git a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/amount.yaml b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/amount.yaml new file mode 100644 index 000000000..4d5600638 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/amount.yaml @@ -0,0 +1,33 @@ +description: > + Weekly amount disregarded in full from the payments a claimant receives for + board and lodging accommodation provided in their home, for each person + provided with it. Payments above this amount are disregarded in part (see the + excess rate). Income Support, income-based Jobseeker's Allowance, + income-related Employment and Support Allowance, Housing Benefit, Pension + Credit and the council tax reduction schemes use it. +metadata: + label: Legacy benefits board and lodging disregard + period: week + unit: currency-GBP + reference: + - title: The Income Support (General) Regulations 1987 Schedule 9 para. 20 + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/20 + - title: The Income Support (General) Amendment Regulations 1990 regs. 1 and 22(c) + href: https://www.legislation.gov.uk/uksi/1990/547/made + - title: The Income-related Benefits Schemes (Miscellaneous Amendments) Regulations 1994 regs. 1(1)(b), 1(3) and 9(4) + href: https://www.legislation.gov.uk/uksi/1994/527/made + - title: The Housing Benefit Regulations 2006 Schedule 5 para. 42 + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/42 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 5 para. 9 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/9 + - title: The State Pension Credit Regulations 2002 Schedule IV para. 8 + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/8 + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 5 para. 9 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/9 +values: + # Income Support Schedule 9 para. 20 has disregarded £20 and half the + # excess since 9 April 1990 (SI 1990/547), restated in its current words + # from 11 April 1994 (SI 1994/527). As made it disregarded £35 of the weekly + # charge where the claimant was employed in providing board and lodging; + # that rule is not modelled. + 1990-04-09: 20 diff --git a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/excess_rate.yaml b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/excess_rate.yaml new file mode 100644 index 000000000..28765f4b5 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/board_and_lodging_disregard/excess_rate.yaml @@ -0,0 +1,22 @@ +description: > + Share of the weekly board and lodging payments above the board and lodging + disregard amount that is also disregarded, for each person provided with + board and lodging accommodation in the claimant's home. +metadata: + label: Legacy benefits board and lodging disregard rate on the excess + unit: /1 + reference: + - title: The Income Support (General) Regulations 1987 Schedule 9 para. 20(b) + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/20 + - title: The Income Support (General) Amendment Regulations 1990 reg. 22(c) + href: https://www.legislation.gov.uk/uksi/1990/547/made + - title: The Housing Benefit Regulations 2006 Schedule 5 para. 42(1)(b) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/42 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 5 para. 9(b) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/9 + - title: The State Pension Credit Regulations 2002 Schedule IV para. 8(1)(b) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/8 + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 5 para. 9(b) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/9 +values: + 1990-04-09: 0.5 diff --git a/policyengine_uk/parameters/gov/hmrc/income_tax/rent_a_room/basic_amount.yaml b/policyengine_uk/parameters/gov/hmrc/income_tax/rent_a_room/basic_amount.yaml new file mode 100644 index 000000000..887f41902 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/income_tax/rent_a_room/basic_amount.yaml @@ -0,0 +1,26 @@ +description: Basic amount of rent-a-room receipts an individual can receive in a tax year free of income tax. An individual who does not meet the exclusive receipts condition, because someone else also receives rent from the residence, has half this amount. +values: + 1992-04-06: + value: 3_250 + reference: + - title: Finance (No. 2) Act 1992 Sch. 10 paras. 6 and 13 + href: https://www.legislation.gov.uk/ukpga/1992/48/schedule/10/enacted + 1997-04-06: + value: 4_250 + reference: + - title: The Income Tax (Furnished Accommodation) (Basic Amount) Order 1996 art. 2 + href: https://www.legislation.gov.uk/uksi/1996/2953/made + - title: Income Tax (Trading and Other Income) Act 2005 s. 789(4) (as enacted) + href: https://www.legislation.gov.uk/ukpga/2005/5/section/789/enacted + 2016-04-06: + value: 7_500 + reference: + - title: The Income Tax (Limit for Rent-a-Room Relief) Order 2015 arts. 1(2) and 2 + href: https://www.legislation.gov.uk/uksi/2015/1539/made +metadata: + label: Rent-a-room relief basic amount + period: year + unit: currency-GBP + reference: + - title: Income Tax (Trading and Other Income) Act 2005 s. 789 + href: https://www.legislation.gov.uk/ukpga/2005/5/section/789 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers.yaml new file mode 100644 index 000000000..3b917fd6b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers.yaml @@ -0,0 +1,384 @@ +# Rent from boarders and lodgers living in the household, in the legacy means +# tests (legacy_benefits_home_letting_income, counted by Income Support, +# Housing Benefit, Pension Credit and council tax reduction). +# +# Board and lodging, per boarder: the first £20 a week is disregarded and half +# the excess (IS Sch 9 para 20; HB Sch 5 para 42; HB (SPC) Sch 5 para 9; SPC Sch +# IV para 8; CTR (England) Sch 5 para 9). Lodging only: £20 a week is +# disregarded. Claimants over the qualifying age for State Pension Credit +# disregard it from "the amount paid by that person" (HB (SPC) Sch 5 para 10; +# SPC Sch IV para 9); working-age claimants from the aggregate paid by "that +# person or a member of his family" (HB Sch 5 para 22; IS Sch 9 para 19), £4 +# before 7 April 2008. Universal Credit does not count this rent (UC Regs 2013 +# reg 66(1); ADM H5112). + +- name: Pension-age householder, one lodger paying £100 a week + # (100 - 20) x 52 = 4,160. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + legacy_benefits_home_letting_income: [4_160, 0] + +- name: One boarder paying £100 a week + # 0.5 x (100 - 20) x 52 = 2,080. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + boarder: + age: 30 + is_household_head: false + rent_paid_as_boarder: 5_200 + benunits: + householder_unit: + members: [householder] + boarder_unit: + members: [boarder] + households: + household: + members: [householder, boarder] + output: + legacy_benefits_home_letting_income: [2_080, 0] + +- name: A boarder paying £15 a week is disregarded in full + # 780 / 52 = 15 < 20. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + boarder: + age: 30 + is_household_head: false + rent_paid_as_boarder: 780 + benunits: + householder_unit: + members: [householder] + boarder_unit: + members: [boarder] + households: + household: + members: [householder, boarder] + output: + legacy_benefits_home_letting_income: [0, 0] + +- name: Pension-age householder, a lodger couple each paying £60 a week + # Per person: (60 - 20) + (60 - 20) = 80 a week; x 52 = 4,160. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + lodger_1: + age: 30 + is_household_head: false + rent_paid_as_lodger: 3_120 + lodger_2: + age: 32 + is_household_head: false + rent_paid_as_lodger: 3_120 + benunits: + householder_unit: + members: [householder] + lodger_couple: + members: [lodger_1, lodger_2] + households: + household: + members: [householder, lodger_1, lodger_2] + output: + legacy_benefits_home_letting_income: [4_160, 0] + +- name: Working-age householder, the same lodger couple + # Aggregate for the family: 120 - 20 = 100 a week; x 52 = 5,200. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + lodger_1: + age: 30 + is_household_head: false + rent_paid_as_lodger: 3_120 + lodger_2: + age: 32 + is_household_head: false + rent_paid_as_lodger: 3_120 + benunits: + householder_unit: + members: [householder] + lodger_couple: + members: [lodger_1, lodger_2] + households: + household: + members: [householder, lodger_1, lodger_2] + output: + legacy_benefits_home_letting_income: [5_200, 0] + +- name: A boarder and a lodger, each paying £100 a week + # Boarder 0.5 x 80 = 40; lodger 80; 120 a week; x 52 = 6,240. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + boarder: + age: 30 + is_household_head: false + rent_paid_as_boarder: 5_200 + lodger: + age: 40 + is_household_head: false + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + boarder_unit: + members: [boarder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, boarder, lodger] + output: + legacy_benefits_home_letting_income: [6_240, 0, 0] + +- name: Working-age lodger disregard was £4 before April 2008 + # 2007-08: lodger (100 - 4) x 52 = 4,992; a boarder at £100 would give + # 0.5 x 80 x 52 = 2,080, here alongside: total 7,072. + period: 2007 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 5_200 + boarder: + age: 31 + is_household_head: false + rent_paid_as_boarder: 5_200 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + boarder_unit: + members: [boarder] + households: + household: + members: [householder, lodger, boarder] + output: + legacy_benefits_home_letting_income: [7_072, 0, 0] + +- name: Universal Credit does not count rent from a lodger + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 35 + is_household_head: true + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + uc_unearned_income: [0, 0] + +- name: Rent between members of a household is not household income + # The lodger's earnings are the household's only market income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 35 + is_household_head: true + lodger: + age: 30 + is_household_head: false + employment_income: 20_000 + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + household_market_income: 20_000 + +- name: Tax on counted rent from a lodger is deducted in the legacy means tests + # 2026-27. Private pension 15,000: (15,000 - 12,570) x 20% = 486. Lodger rent + # 10,400 is rent-a-room receipts; 10,400 - 7,500 = 2,900 taxable, x 20% = + # 580. Income tax 1,066, all of it on income the means tests count (IS Sch 9 + # para 1; HB Sch 5 para 1; SPC reg 17), so all of it is deducted. Counted + # rent: (200 - 20) x 52 = 9,360. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + private_pension_income: 15_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 10_400 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + income_tax: [1_066, 0] + property_income_tax: [580, 0] + rent_a_room_income_tax: [580, 0] + legacy_means_test_income_tax: [1_066, 0] + legacy_benefits_home_letting_income: [9_360, 0] + +- name: Tax on counted rent-a-room income shares a tax reduction pro rata + # 2026-27. Step 4 tax: private pension (36,000 - 12,570) x 20% = 4,686; + # rent-a-room income (10,400 - 7,500) x 20% = 580; savings interest + # (3,000 - 1,000 personal savings allowance) x 20% = 400, with no starting + # rate for savings because non-savings income exceeds the allowances by more + # than £5,000; total 5,666. The married couple's allowance reduction + # (11,000 x 10% = 1,100, set as an input) leaves income tax of 4,566. The + # tests count the pension and the rent but treat interest as capital: the + # capital tax of 400 keeps its share of the reduction, + # 1,100 x 400 / 5,666 = 77.66, so 322.34 is not deducted, and the tax + # deducted is 4,566 - 322.34 = 4,243.66. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + private_pension_income: 36_000 + savings_interest_income: 3_000 + married_couples_allowance: 11_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 10_400 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + rent_a_room_income_tax: [580, 0] + savings_income_tax: [400, 0] + capped_mcad: [1_100, 0] + income_tax: [4_566, 0] + legacy_means_test_income_tax: [4_243.66, 0] + +- name: UC does not deduct the tax on rent it does not count + # 2026-27, England. Earnings 20,000: tax (20,000 - 12,570) x 20% = 1,486, + # NI 7,430 x 8% = 594.40. Lodger rent 10,000 gives taxable rent-a-room income + # 2,500 and tax 500, so income tax is 1,986. UC counts neither the rent + # (reg 66(1)) nor its tax (reg 55(5)(b)): earned income + # 20,000 - 1,486 - 594.40 = 17,919.60, as without the lodger. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 20_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 10_000 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + region: NORTH_WEST + output: + income_tax: [1_986, 0] + rent_a_room_income_tax_after_reductions: [500, 0] + uc_earned_income: [17_919.60, 0] + +- name: Welsh CTR for a UC recipient does not add rent from a lodger + # WSI 2013/3029 Sch 6 para 9: with a UC award, income is the UC assessment, + # which excludes this rent. Here UC (5,000, set as an input) is the only + # income, so CTR applicable income is 5,000, not 5,000 + 4,160. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + universal_credit: 5_000 + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + region: WALES + output: + legacy_benefits_home_letting_income: [4_160, 0] + council_tax_reduction_applicable_income: [5_000, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers_non_dependants.yaml new file mode 100644 index 000000000..59a7ed221 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers_non_dependants.yaml @@ -0,0 +1,58 @@ +# A boarder or lodger is liable to pay the householder on a commercial basis +# for their occupation, so is not a non-dependant: UC Regs 2013 Sch 4 para +# 9(2)(d); HB Regs 2006 reg 3(2)(e)(i); CTR (England) prescribed requirements +# reg 9(2)(e). An unrelated adult in the household who pays nothing remains a +# non-dependant. + +- name: An adult lodger is not a non-dependant + period: 2026 + input: + people: + householder: + age: 45 + is_household_head: true + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 4_000 + benunits: + householder_unit: + members: [householder] + benunit_rent: 9_000 + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + tenure_type: RENT_PRIVATELY + output: + pays_rent_to_householder: [false, true] + uc_individual_non_dep_deduction_eligible: [false, false] + housing_benefit_individual_non_dep_deduction_eligible: [false, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] + +- name: The same adult paying nothing is a non-dependant + period: 2026 + input: + people: + householder: + age: 45 + is_household_head: true + adult: + age: 30 + is_household_head: false + benunits: + householder_unit: + members: [householder] + benunit_rent: 9_000 + adult_unit: + members: [adult] + households: + household: + members: [householder, adult] + tenure_type: RENT_PRIVATELY + output: + pays_rent_to_householder: [false, false] + uc_individual_non_dep_deduction_eligible: [false, true] + housing_benefit_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.yaml new file mode 100644 index 000000000..6160dbe27 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.yaml @@ -0,0 +1,211 @@ +# Rent-a-room relief (ITTOIA 2005 Part 7 Chapter 1). Receipts up to the +# individual's limit are not charged (s. 793). Above it the model assumes the +# s. 800 election: the limit is deducted and no expenses are allowed (s. 797). +# The basic amount is £4,250 (s. 789(4) as enacted) and £7,500 from 2016-17 +# (SI 2015/1539 arts. 1(2), 2), halved where someone else also receives rent +# from the residence (ss. 789(3), 790). Rent-a-room receipts are not relievable +# receipts for the £1,000 property allowance (s. 783BB(2)). +# +# Tax figures, 2026-27: personal allowance £12,570; basic rate 20%. + +- name: Rent from a lodger within the limit is not taxed + # Receipts 6,000 <= 7,500, so nothing is charged. Earnings tax: + # (30,000 - 12,570) x 20% = 3,486. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 30_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 6_000 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + rent_from_boarders_and_lodgers: [6_000, 0] + rent_a_room_receipts: [6_000, 0] + rent_a_room_relief: [6_000, 0] + taxable_rent_a_room_income: [0, 0] + taxable_property_income: [0, 0] + income_tax: [3_486, 0] + +- name: Rent from a lodger above the limit is taxed on the excess + # Taxable: 10,000 - 7,500 = 2,500, taxed at 20% = 500 on top of 3,486. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 30_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 10_000 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + rent_a_room_relief: [7_500, 0] + taxable_rent_a_room_income: [2_500, 0] + taxable_property_income: [2_500, 0] + rent_a_room_income_tax: [500, 0] + income_tax: [3_986, 0] + +- name: The property allowance does not reduce taxable rent-a-room income + # Other property income 500 is covered by the property allowance + # (min(500, 1,000)); the rent-a-room excess of 2,500 is not. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 30_000 + property_income: 500 + boarder: + age: 30 + is_household_head: false + rent_paid_as_boarder: 10_000 + benunits: + householder_unit: + members: [householder] + boarder_unit: + members: [boarder] + households: + household: + members: [householder, boarder] + output: + property_allowance_deduction: [500, 0] + taxable_rent_a_room_income: [2_500, 0] + taxable_property_income: [2_500, 0] + +- name: The limit was 4,250 before 2016-17 + # 6,000 - 4,250 = 1,750. + period: 2015 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 30_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 6_000 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + rent_a_room_limit: [4_250, 4_250] + rent_a_room_relief: [4_250, 0] + taxable_rent_a_room_income: [1_750, 0] + +- name: Two people receiving rent from the home each have half the limit + # The householder receives the lodger's 6,000; the partner sublets part of + # the home for 2,000. Each limit is 7,500 / 2 = 3,750. Householder: + # 6,000 - 3,750 = 2,250 taxable; partner: 2,000 < 3,750, nothing taxable. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + partner: + age: 44 + is_household_head: false + sublet_income: 2_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 6_000 + benunits: + couple: + members: [householder, partner] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, partner, lodger] + output: + rent_a_room_receipts: [6_000, 2_000, 0] + # The lodger receives nothing, so keeps the basic amount. + rent_a_room_limit: [3_750, 3_750, 7_500] + taxable_rent_a_room_income: [2_250, 0, 0] + +- name: Rent paid by someone in the householder's own benefit unit is not received + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + partner: + age: 44 + is_household_head: false + rent_paid_as_lodger: 6_000 + benunits: + couple: + members: [householder, partner] + households: + household: + members: [householder, partner] + output: + rent_from_boarders_and_lodgers: [0, 0] + pays_rent_to_householder: [false, false] + +- name: Tax credits count only the taxable rent-a-room income + # 2023-24. Taxable rent-a-room income: 10,000 - 7,500 = 2,500. Step 1 income + # (pensions, savings, dividends, property): 2,500 less the £300 disregard = + # 2,200. Step 2: earnings 10,000. Total 12,200. + period: 2023 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + employment_income: 10_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 10_000 + benunits: + householder_unit: + members: [householder] + income_support: 0 + esa_income: 0 + jsa_income: 0 + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + output: + taxable_rent_a_room_income: [2_500, 0] + tax_credits_applicable_income: [12_200, 0] diff --git a/policyengine_uk/tests/test_boarder_lodger_rent_properties.py b/policyengine_uk/tests/test_boarder_lodger_rent_properties.py new file mode 100644 index 000000000..e1b963bfa --- /dev/null +++ b/policyengine_uk/tests/test_boarder_lodger_rent_properties.py @@ -0,0 +1,283 @@ +"""Property-based tests for rent from boarders, lodgers and sub-tenants. + +A household head may receive rent from boarders and lodgers who live in the +household in their own benefit units (rent_paid_as_boarder and +rent_paid_as_lodger, recorded on the payer), and from a sub-tenant outside it +(sublet_income). + +Invariants, for any generated household: + +1. Conservation: the rent the head receives equals the rent paid by members + outside the head's benefit unit, and nobody else receives any. +2. Rent-a-room bounds: 0 <= relief <= receipts; the limit is half the basic + amount exactly when the person and someone else in the household both + have receipts, and the basic amount otherwise; taxable rent-a-room income + = max(0, receipts - limit). +3. Disregards: the counted home-letting income is between nil and the rent + received, and equals the statutory closed form (boarders: half the excess + over £20 a week each; lodgers: the excess over £20 a week, per person at + pension age and per lodger family at working age; sub-tenants: the excess + over £20 a week). +4. Monotone: more rent from a boarder or lodger who already pays never lowers + taxable rent-a-room income or counted home-letting income, and never raises + Income Support, Housing Benefit, Pension Credit or council tax reduction. +5. Boarders keep more: the same weekly amount counts no more when paid for + board and lodging than when paid for lodging only. +6. Universal Credit unearned income and household market income do not depend + on the rent paid between members of the household. +7. Where a programme does not count this rent, it does not deduct the tax on + it either: UC earned income, and council tax reduction income under the + Scottish working-age scheme and the Welsh universal credit route, do not + change when the rent changes. +8. Tax: legacy_means_test_income_tax is between nil and income tax. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +YEARS = [2025, 2026] +WEEKS = 52 +DISREGARD = 20 +BASIC_AMOUNT = 7_500 +AWARDS = ["income_support", "housing_benefit", "pension_credit", "council_tax_benefit"] +rent = st.one_of(st.just(0.0), st.floats(100, 20_000)) + + +@st.composite +def households(draw): + pension_age = draw(st.booleans()) + age = st.integers(67, 90) if pension_age else st.integers(25, 60) + head_unit = [ + dict(age=draw(age), employment_income=draw(st.floats(0, 25_000))) + for _ in range(draw(st.integers(1, 2))) + ] + payers = [] + for _ in range(draw(st.integers(0, 2))): + kind = draw(st.sampled_from(["rent_paid_as_boarder", "rent_paid_as_lodger"])) + payers.append( + [ + {"age": draw(st.integers(19, 60)), kind: draw(rent)} + for _ in range(draw(st.integers(1, 2))) + ] + ) + return dict( + head_unit=head_unit, + payers=payers, + sublet=draw(st.one_of(st.just(0.0), st.floats(0, 8_000))), + partner_sublet=draw(st.one_of(st.just(0.0), st.floats(100, 8_000))), + tenure=draw(st.sampled_from(["RENT_PRIVATELY", "OWNED_OUTRIGHT"])), + region=draw(st.sampled_from(["NORTH_WEST", "LONDON", "WALES", "SCOTLAND"])), + ) + + +def situation(h, year, scale=1.0, as_kind=None): + people, benunits = {}, {} + names = [] + for i, attrs in enumerate(h["head_unit"]): + name = f"head_{i}" + people[name] = { + "age": {year: attrs["age"]}, + "employment_income": {year: attrs["employment_income"]}, + "is_household_head": {year: i == 0}, + "sublet_income": {year: h["sublet"] if i == 0 else h["partner_sublet"]}, + } + names.append(name) + benunits["head_unit"] = {"members": list(names)} + for j, unit in enumerate(h["payers"]): + members = [] + for k, attrs in enumerate(unit): + name = f"payer_{j}_{k}" + person = {"age": {year: attrs["age"]}, "is_household_head": {year: False}} + for kind in ("rent_paid_as_boarder", "rent_paid_as_lodger"): + if kind in attrs: + target = as_kind or kind + person[target] = {year: attrs[kind] * scale} + people[name] = person + members.append(name) + benunits[f"payer_{j}"] = {"members": members} + names += members + return { + "people": people, + "benunits": benunits, + "households": { + "household": { + "members": names, + "tenure_type": {year: h["tenure"]}, + "region": {year: h["region"]}, + "rent": {year: 8_000.0 if h["tenure"] == "RENT_PRIVATELY" else 0.0}, + "council_tax": {year: 1_500.0}, + } + }, + } + + +def calculate(h, year, variables, **kwargs): + sim = Simulation(situation=situation(h, year, **kwargs)) + return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in variables} + + +def per_week(annual): + return annual / WEEKS + + +def payments(h, kind=None): + return [ + [ + a.get(kind, 0.0) if kind else sum(v for k, v in a.items() if k != "age") + for a in unit + ] + for unit in h["payers"] + ] + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_rent_is_received_by_the_head_and_conserved(h, year): + v = calculate(h, year, ["rent_from_boarders_and_lodgers"]) + received = v["rent_from_boarders_and_lodgers"] + paid = sum(sum(unit) for unit in payments(h)) + np.testing.assert_allclose(received.sum(), paid, rtol=1e-6, atol=0.01) + assert np.all(received[1:] == 0) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_rent_a_room_bounds(h, year): + v = calculate( + h, + year, + [ + "rent_a_room_receipts", + "rent_a_room_limit", + "rent_a_room_relief", + "taxable_rent_a_room_income", + ], + ) + receipts, limit = v["rent_a_room_receipts"], v["rent_a_room_limit"] + receives = receipts > 0 + shared = receives & (receives.sum() - receives > 0) + np.testing.assert_allclose( + limit, np.where(shared, BASIC_AMOUNT / 2, BASIC_AMOUNT), atol=0.01 + ) + assert np.all(v["rent_a_room_relief"] >= 0) + assert np.all(v["rent_a_room_relief"] <= receipts + 0.01) + np.testing.assert_allclose( + v["taxable_rent_a_room_income"], np.maximum(0, receipts - limit), atol=0.01 + ) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_home_letting_income_closed_form(h, year): + sim = Simulation(situation=situation(h, year)) + counted = np.asarray(sim.calculate("legacy_benefits_home_letting_income", year)) + pension_age = bool( + np.asarray(sim.calculate("is_SP_age", year))[: len(h["head_unit"])].any() + ) + board = sum( + 0.5 * max(0, per_week(x) - DISREGARD) + for unit in payments(h, "rent_paid_as_boarder") + for x in unit + ) + lodgers = payments(h, "rent_paid_as_lodger") + if pension_age: + lodging = sum(max(0, per_week(x) - DISREGARD) for unit in lodgers for x in unit) + else: + lodging = sum(max(0, per_week(sum(unit)) - DISREGARD) for unit in lodgers) + sublet_received = h["sublet"] + ( + h["partner_sublet"] if len(h["head_unit"]) > 1 else 0.0 + ) + # #1995 sums the claimant's and partner's sublet income before one + # disregard (one occupier assumed). + sublet = max(0, per_week(sublet_received) - DISREGARD) + expected = (board + lodging + sublet) * WEEKS + np.testing.assert_allclose(counted[0], expected, rtol=1e-6, atol=0.01) + assert np.all(counted[1:] == 0) + received = sum(sum(unit) for unit in payments(h)) + sublet_received + assert 0 <= counted[0] <= received + 0.01 + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS), st.floats(1.1, 4)) +def test_more_rent_is_monotone(h, year, scale): + variables = [ + "taxable_rent_a_room_income", + "legacy_benefits_home_letting_income", + ] + AWARDS + base = calculate(h, year, variables) + more = calculate(h, year, variables, scale=scale) + for v in ["taxable_rent_a_room_income", "legacy_benefits_home_letting_income"]: + assert np.all(more[v] >= base[v] - 0.01), v + for v in AWARDS: + assert np.all(more[v] <= base[v] + 0.01), v + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_board_and_lodging_counts_no_more_than_lodging(h, year): + v = "legacy_benefits_home_letting_income" + as_boarders = calculate(h, year, [v], as_kind="rent_paid_as_boarder")[v] + as_lodgers = calculate(h, year, [v], as_kind="rent_paid_as_lodger")[v] + pension_age = calculate(h, year, ["is_SP_age"])["is_SP_age"][ + : len(h["head_unit"]) + ].any() + if pension_age: + # Per-person lodger disregard at pension age: boarders keep at least + # as much per person. + assert np.all(as_boarders <= as_lodgers + 0.01) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_rent_within_the_household_changes_no_uc_income_or_market_income(h, year): + variables = ["uc_unearned_income", "household_market_income"] + with_rent = calculate(h, year, variables) + without = calculate(h, year, variables, scale=0.0) + for v in variables: + np.testing.assert_allclose(with_rent[v], without[v], atol=0.01, err_msg=v) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_legacy_means_test_income_tax_bounds(h, year): + v = calculate(h, year, ["legacy_means_test_income_tax", "income_tax"]) + assert np.all(v["legacy_means_test_income_tax"] >= 0) + assert np.all(v["legacy_means_test_income_tax"] <= v["income_tax"] + 0.01) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS), st.floats(0.3, 0.9)) +def test_programmes_that_exclude_the_rent_ignore_its_tax(h, year, scale): + # Both runs keep every payment positive, so non-dependant status is fixed. + variables = [ + "uc_earned_income", + "council_tax_reduction_applicable_income", + "universal_credit", + "is_SP_age", + ] + full = calculate(h, year, variables) + part = calculate(h, year, variables, scale=scale) + np.testing.assert_allclose( + full["uc_earned_income"], part["uc_earned_income"], atol=0.01 + ) + head_unit = 0 + working_age = not full["is_SP_age"][: len(h["head_unit"])].any() + on_uc = full["universal_credit"][head_unit] > 0 + excluded = (h["region"] == "SCOTLAND" and (working_age or on_uc)) or ( + h["region"] == "WALES" and on_uc + ) + if excluded and (part["universal_credit"][head_unit] > 0) == on_uc: + np.testing.assert_allclose( + full["council_tax_reduction_applicable_income"][head_unit], + part["council_tax_reduction_applicable_income"][head_unit], + atol=0.01, + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py index 37ea75a31..00913bc2a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py @@ -6,9 +6,13 @@ class housing_benefit_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for the Housing Benefit non-dependent deduction" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/3" def formula(person, period, parameters): rent_liable = person.benunit("benunit_is_rent_liable", period) p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction age_eligible = person("age", period) >= p.age_threshold - return age_eligible & ~rent_liable + # A boarder or lodger is liable to pay the claimant on a commercial + # basis for their occupation, so is not a non-dependant (reg 3(2)(e)(i)). + commercial = person("pays_rent_to_householder", period) + return age_eligible & ~rent_liable & ~commercial diff --git a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py index 2cf125e31..aa9a4be53 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py +++ b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py @@ -7,19 +7,23 @@ class legacy_benefits_home_letting_income(Variable): label = "Rent from letting part of the home counted in the legacy means tests" documentation = ( "Rent the claimant and any partner receive for letting part of the home " - "they live in (sublet_income), after the weekly sub-tenant disregard. " - "The legacy means tests treat other income derived from capital, such as " - "rent from other property, interest and dividends, as capital, but the " - "home is disregarded capital, so rent for part of it stays income: " - "Income Support Schedule 9 paragraph 19, Housing Benefit Schedule 5 " - "paragraph 22, and for claimants over the qualifying age for State " - "Pension Credit Pension Credit regulation 15(5)(i) with Schedule IV " - "paragraph 9 and pension-age Housing Benefit regulation 29(1)(v) with " - "Schedule 5 paragraph 10. The disregard applies per occupier; the data " - "give one annual amount and no count of occupiers, so the model assumes " - "one. The pension-age amount applies when the claimant or partner is " - "over State Pension age, which stands in for the qualifying age for " - "State Pension Credit; the two amounts have been equal since April " + "they live in, after the weekly disregards. The legacy means tests treat " + "other income derived from capital, such as rent from other property, " + "interest and dividends, as capital, but the home is disregarded " + "capital, so rent for part of it stays income. Three sources count. " + "(1) Rent from a sub-tenant outside the household (sublet_income), less " + "the sub-tenant disregard; the data give one annual amount and no count " + "of occupiers, so the model assumes one. (2) Rent from lodgers in the " + "household, paid to the household head, less the same disregard: per " + "person for claimants over the qualifying age for State Pension Credit, " + "whose rules disregard 'the amount paid by that person', and per lodger " + "family for working-age claimants, whose rules aggregate payments by " + "'that person or a member of his family'. (3) Payments for board and " + "lodging, paid to the household head, less the board and lodging " + "disregard for each boarder: the first £20 a week and half the excess. " + "The pension-age amounts apply when the claimant or partner is over " + "State Pension age, which stands in for the qualifying age for State " + "Pension Credit; the sub-tenant amounts have been equal since April " "2008. The calculation is annual, so a year with an April change uses " "the amount in force at the start of the year." ) @@ -27,21 +31,61 @@ class legacy_benefits_home_letting_income(Variable): unit = GBP reference = [ "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/19", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/20", "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/22", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/42", "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/9", "https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/10", "https://www.legislation.gov.uk/uksi/2002/1792/regulation/15", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/8", "https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/9", ] def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.legacy_means_tests.sub_tenant_rent_disregard + p = parameters(period).gov.dwp.legacy_means_tests + sub_tenant = p.sub_tenant_rent_disregard + board = p.board_and_lodging_disregard person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) - weekly_rent = ( + pension_age = benunit.any(person("is_SP_age", period) & claimant_or_partner) + + weekly_sublet = ( benunit.sum(person("sublet_income", period) * claimant_or_partner) / WEEKS_IN_YEAR ) - pension_age = benunit.any(person("is_SP_age", period) & claimant_or_partner) - disregard = where(pension_age, p.pension_age, p.working_age) - return max_(0, weekly_rent - disregard) * WEEKS_IN_YEAR + sublet_disregard = where( + pension_age, sub_tenant.pension_age, sub_tenant.working_age + ) + counted_sublet = max_(0, weekly_sublet - sublet_disregard) + + # Boarders and lodgers are household members outside the head's + # benefit unit; what each pays counts for the head's benefit unit when + # the head is its claimant or partner. + head = person("is_household_head", period) + pays_head = ~person.benunit.any(head) + boarder = person("rent_paid_as_boarder", period) / WEEKS_IN_YEAR * pays_head + lodger = person("rent_paid_as_lodger", period) / WEEKS_IN_YEAR * pays_head + counted_board = (1 - board.excess_rate) * max_(0, boarder - board.amount) + counted_lodger_pension_age = max_(0, lodger - sub_tenant.pension_age) + family_lodger = person.benunit.sum(lodger) + family_share = np.divide( + lodger, + family_lodger, + out=np.zeros_like(lodger), + where=family_lodger > 0, + ) + counted_lodger_working_age = ( + max_(0, family_lodger - sub_tenant.working_age) * family_share + ) + household = person.household + from_occupiers = where( + pension_age, + benunit.max(household.sum(counted_board + counted_lodger_pension_age)), + benunit.max(household.sum(counted_board + counted_lodger_working_age)), + ) + # Split by heads if more than one is flagged. + head_share = benunit.sum(head & claimant_or_partner) / max_( + benunit.max(household.sum(head)), 1 + ) + return (counted_sublet + from_occupiers * head_share) * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py index f8139532e..2971d9af3 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py @@ -13,7 +13,9 @@ class legacy_means_test_income_tax(Variable): "council tax reduction schemes' equivalents), and they treat savings " "interest, dividends and property income as capital, not income. So this " "is the tax calculated on the person's other income (earned_income_tax, " - "which covers earnings, pensions and taxable benefits) at Step 4 of " + "which covers earnings, pensions and taxable benefits, plus the tax on " + "rent-a-room income above the limit: rent from boarders, lodgers and " + "sub-tenants in the home is income the tests count) at Step 4 of " "Income Tax Act 2007 s.23, less that income's share of the Step 6 tax " "reductions (the married couple's allowance and other tax credits). " "Neither tax law nor the benefit regulations say which income a " @@ -41,11 +43,17 @@ def formula(person, period, parameters): p = parameters(period).gov.hmrc.income_tax # Step 4 tax on the income the means tests count, and on the income # they treat as capital. - counted = person("earned_income_tax", period) - capital = add( - person, - period, - ["savings_income_tax", "dividend_income_tax", "property_income_tax"], + # Tax on rent-a-room income sits inside property income tax, but the + # tests count that rent, so its tax moves from capital to counted. + rent_a_room_tax = person("rent_a_room_income_tax", period) + counted = person("earned_income_tax", period) + rent_a_room_tax + capital = ( + add( + person, + period, + ["savings_income_tax", "dividend_income_tax", "property_income_tax"], + ) + - rent_a_room_tax ) before_reductions = counted + capital # Step 7 charges: everything income tax adds beyond the Step 4 tax. 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 1c698c192..1f954fc76 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -26,6 +26,8 @@ def formula(benunit, period, parameters): "savings_interest_income", "dividend_income", "property_income", + # Rent-a-room receipts count only where income tax charges them. + "taxable_rent_a_room_income", ] 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/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py index abd986925..a42dbbe2f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py @@ -6,6 +6,7 @@ class uc_individual_non_dep_deduction_eligible(Variable): entity = Person label = "Eligible person for the Universal Credit non-dependent deduction" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9" def formula(person, period, parameters): not_rent_liable = ~person.benunit("benunit_is_rent_liable", period) @@ -15,4 +16,7 @@ def formula(person, period, parameters): age = person("age", period) age_eligible = age >= p.age_threshold exempt = person("uc_non_dep_deduction_exempt", period) - return ~exempt & age_eligible & not_rent_liable + # A boarder or lodger is liable to pay on a commercial basis for their + # occupation, so is not a non-dependant (Sch 4 para 9(2)(d)). + commercial = person("pays_rent_to_householder", period) + return ~exempt & age_eligible & not_rent_liable & ~commercial diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py index 6e496b868..538f10fe0 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py @@ -28,4 +28,11 @@ def formula(benunit, period, parameters): ["uc_work_allowance", "tax", "pension_contributions"], members, ) + # UC does not count rent from boarders, lodgers or sub-tenants (reg + # 66(1)), so the tax on it is not tax in respect of any employment + # (reg 55(5)(b)) and is not deducted. The rest of `tax` is wider than + # reg 55(5)(b) allows; that is #1942. + disregards -= add_for_members( + benunit, period, ["rent_a_room_income_tax_after_reductions"], members + ) return max_(0, personal_gross_earned_income - disregards) 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 d70b06548..4f3876ba0 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,11 +5,23 @@ 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 income less the property allowance, plus rent-a-room " + "receipts above the rent-a-room limit, which are profits of the same " + "UK property business but are not relievable receipts for the " + "property allowance." ) + 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. 783BB(2)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BB", + ), + ] unit = GBP def formula(person, period, parameters): @@ -17,4 +29,4 @@ def formula(person, period, parameters): 0, person("property_income", period) - person("property_allowance_deduction", period), - ) + ) + person("taxable_rent_a_room_income", period) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax.py new file mode 100644 index 000000000..d8c3b0d72 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class rent_a_room_income_tax(Variable): + value_type = float + entity = Person + label = "income tax on rent-a-room income" + documentation = ( + "The share of income tax on property income that falls on taxable " + "rent-a-room income, in proportion to its share of taxable property " + "income. The legacy means tests count rent-a-room receipts as income " + "and disregard the tax paid on income they count." + ) + definition_period = YEAR + unit = GBP + + def formula(person, period, parameters): + taxable_rent_a_room = person("taxable_rent_a_room_income", period) + taxable_property = person("taxable_property_income", period) + share = np.divide( + taxable_rent_a_room, + taxable_property, + out=np.zeros_like(taxable_property), + where=taxable_property > 0, + ) + return person("property_income_tax", period) * share diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax_after_reductions.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax_after_reductions.py new file mode 100644 index 000000000..85e6c7606 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax_after_reductions.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class rent_a_room_income_tax_after_reductions(Variable): + value_type = float + entity = Person + label = "income tax on rent-a-room income after tax reductions" + documentation = ( + "The tax on taxable rent-a-room income less its share of the Step 6 " + "tax reductions, shared in proportion to the Step 4 tax on each kind " + "of income as legacy_means_test_income_tax does. It is the part of " + "income tax paid on this rent, which programmes that do not count the " + "rent must not deduct from the income they do count." + ) + definition_period = YEAR + unit = GBP + reference = "https://www.legislation.gov.uk/ukpga/2007/3/section/23" + + def formula(person, period, parameters): + p = parameters(period).gov.hmrc.income_tax + rent_a_room_tax = person("rent_a_room_income_tax", period) + before_reductions = add( + person, + period, + [ + "earned_income_tax", + "savings_income_tax", + "dividend_income_tax", + "property_income_tax", + ], + ) + reductions = min_( + add(person, period, p.income_tax_subtractions), before_reductions + ) + share = np.divide( + rent_a_room_tax, + before_reductions, + out=np.zeros_like(rent_a_room_tax, dtype=float), + where=before_reductions > 0, + ) + return rent_a_room_tax - reductions * share diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_limit.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_limit.py new file mode 100644 index 000000000..06f51f696 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_limit.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class rent_a_room_limit(Variable): + value_type = float + entity = Person + label = "rent-a-room relief limit" + documentation = ( + "The basic amount, or half of it where the individual does not meet " + "the exclusive receipts condition. The model takes the condition to " + "fail where the individual and another member of the household both " + "have rent-a-room receipts from the home." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ukpga/2005/5/section/789", + "https://www.legislation.gov.uk/ukpga/2005/5/section/790", + ] + + def formula(person, period, parameters): + basic_amount = parameters(period).gov.hmrc.income_tax.rent_a_room.basic_amount + receives = person("rent_a_room_receipts", period) > 0 + others_receive = person.household.sum(receives) - receives > 0 + return where(receives & others_receive, basic_amount / 2, basic_amount) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_receipts.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_receipts.py new file mode 100644 index 000000000..d01c781e2 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_receipts.py @@ -0,0 +1,18 @@ +from policyengine_uk.model_api import * + + +class rent_a_room_receipts(Variable): + value_type = float + entity = Person + label = "rent-a-room receipts" + documentation = ( + "Receipts for the use of furnished accommodation in the individual's " + "only or main residence, including meals and other services supplied " + "with it: rent from boarders and lodgers who live in the household, " + "and rent from letting part of the home to someone outside it. The " + "model treats all of it as furnished accommodation." + ) + definition_period = YEAR + unit = GBP + reference = "https://www.legislation.gov.uk/ukpga/2005/5/section/786" + adds = ["rent_from_boarders_and_lodgers", "sublet_income"] diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.py new file mode 100644 index 000000000..bb2a8c263 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_relief.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class rent_a_room_relief(Variable): + value_type = float + entity = Person + label = "rent-a-room relief" + documentation = ( + "Rent-a-room receipts not charged to income tax. Receipts up to the " + "limit are not brought into account (full relief). Above the limit " + "the model assumes the individual elects for the alternative method, " + "under which the limit is deducted from the receipts and no expenses " + "are allowed; the data hold no expenses for this income." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ukpga/2005/5/section/793", + "https://www.legislation.gov.uk/ukpga/2005/5/section/797", + "https://www.legislation.gov.uk/ukpga/2005/5/section/800", + ] + + def formula(person, period, parameters): + return min_( + person("rent_a_room_receipts", period), + person("rent_a_room_limit", period), + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/taxable_rent_a_room_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/taxable_rent_a_room_income.py new file mode 100644 index 000000000..b212e2e4d --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/taxable_rent_a_room_income.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class taxable_rent_a_room_income(Variable): + value_type = float + entity = Person + label = "taxable rent-a-room income" + documentation = ( + "Rent-a-room receipts above the individual's limit, brought into " + "account as profits of a UK property business. They are not " + "relievable receipts for the property allowance." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ukpga/2005/5/section/797", + "https://www.legislation.gov.uk/ukpga/2005/5/section/783BB", + ] + + def formula(person, period, parameters): + return person("rent_a_room_receipts", period) - person( + "rent_a_room_relief", period + ) diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py b/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py index f13a71fb4..a7daa17e7 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/total_income.py @@ -18,6 +18,7 @@ class total_income(Variable): "social_security_income", "self_employment_income", "property_income", + "taxable_rent_a_room_income", "savings_interest_income", "dividend_income", "miscellaneous_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 53bba409a..dc402a4ac 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 @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_scotland_scheme, + is_wales_scheme, ) @@ -113,9 +114,18 @@ def formula(benunit, period, parameters): p_scotland = parameters( period ).gov.local_authorities.scotland.council_tax_reduction.means_test - counts_home_letting = ~scottish_working_age_scheme | ( - p_scotland.working_age_counts_home_letting_income - ) + # Wales: for an applicant with an award of universal credit (who is + # never a pensioner, WSI 2013/3029 reg 3), income is the Secretary of + # State's universal credit assessment (Sch 6 para 9), which has no + # rent from part of the home. That route is otherwise not modelled + # here (#1966); this keeps such rent out of it. + wales_universal_credit_route = is_wales_scheme( + benunit.household("country", period) + ) & (add(benunit, period, ["universal_credit"]) > 0) + counts_home_letting = ( + ~scottish_working_age_scheme + | p_scotland.working_age_counts_home_letting_income + ) & ~wales_universal_credit_route increased_income += where( counts_home_letting, benunit("legacy_benefits_home_letting_income", period), @@ -136,6 +146,14 @@ def formula(benunit, period, parameters): ["legacy_means_test_income_tax", "national_insurance"], members, ) + # Where the rent is not counted, neither is the tax on it. + tax -= where( + counts_home_letting, + 0, + add_for_members( + benunit, period, ["rent_a_room_income_tax_after_reductions"], members + ), + ) income_under_general_rules = max_( 0, increased_income - tax - pension_contributions ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 36cb046b4..6323da1c4 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -6,8 +6,15 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for CTR non-dependent deduction" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9" def formula(person, period, parameters): - return (person("age", period) >= 18) & ~person.benunit( - "benunit_contains_household_head", period + # A boarder or lodger is liable to pay the applicant on a commercial + # basis for their occupation, so is not a non-dependant (SI 2012/2885 + # reg 9(2)(e)). + commercial = person("pays_rent_to_householder", period) + return ( + (person("age", period) >= 18) + & ~person.benunit("benunit_contains_household_head", period) + & ~commercial ) diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 5061ab23c..5dfe28cbb 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -10,6 +10,7 @@ "dividend_income", "miscellaneous_income", "property_income", + "sublet_income", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/household_market_income.py b/policyengine_uk/variables/household/income/household_market_income.py index 3991db6b7..e531eabd5 100644 --- a/policyengine_uk/variables/household/income/household_market_income.py +++ b/policyengine_uk/variables/household/income/household_market_income.py @@ -10,6 +10,7 @@ "dividend_income", "miscellaneous_income", "property_income", + "sublet_income", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/market_income.py b/policyengine_uk/variables/household/income/market_income.py index 9df8de09c..0fbd44bb6 100644 --- a/policyengine_uk/variables/household/income/market_income.py +++ b/policyengine_uk/variables/household/income/market_income.py @@ -19,6 +19,7 @@ def formula(person, period, parameters): "dividend_income", "miscellaneous_income", "property_income", + "sublet_income", "private_pension_income", "private_transfer_income", "maintenance_income", diff --git a/policyengine_uk/variables/household/income/pays_rent_to_householder.py b/policyengine_uk/variables/household/income/pays_rent_to_householder.py new file mode 100644 index 000000000..75b47f92c --- /dev/null +++ b/policyengine_uk/variables/household/income/pays_rent_to_householder.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class pays_rent_to_householder(Variable): + value_type = bool + entity = Person + label = "in a family that pays the householder rent as boarders or lodgers" + documentation = ( + "Whether this person's benefit unit, which does not contain the " + "household head, pays the household head rent for board and lodging " + "or for lodging. Such a person is liable to make payments on a " + "commercial basis for their occupation, so is not a non-dependant of " + "the householder. The model takes every such payment to be commercial " + "and the payer not to be a close relative living there, whom the " + "rules keep as a non-dependant (HB reg 3(3) with reg 9(1); CTR reg " + "9(3)). The Family Resources Survey asks about this rent only for " + "people not related to the household reference person." + ) + definition_period = YEAR + reference = [ + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + ] + + def formula(person, period, parameters): + paid = add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) + in_head_benunit = person.benunit.any(person("is_household_head", period)) + return person.benunit.any(paid > 0) & ~in_head_benunit diff --git a/policyengine_uk/variables/household/income/rent_from_boarders_and_lodgers.py b/policyengine_uk/variables/household/income/rent_from_boarders_and_lodgers.py new file mode 100644 index 000000000..e2aa086a3 --- /dev/null +++ b/policyengine_uk/variables/household/income/rent_from_boarders_and_lodgers.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class rent_from_boarders_and_lodgers(Variable): + value_type = float + entity = Person + label = "rent received from boarders and lodgers" + documentation = ( + "Rent the household head receives from boarders and lodgers: the " + "rent paid as a boarder or lodger by household members outside the " + "head's benefit unit, shared equally where more than one person is " + "flagged as household head (the survey data have one; with more, the " + "means tests share the rent after the disregards, not before). The " + "Family Resources Survey records this rent only on the payer. It is a transfer within the household, so " + "household income measures do not include it; it enters income tax " + "as rent-a-room receipts and the legacy means tests through the " + "home-letting income." + ) + definition_period = YEAR + unit = GBP + + def formula(person, period, parameters): + paid = add(person, period, ["rent_paid_as_boarder", "rent_paid_as_lodger"]) + head = person("is_household_head", period) + in_head_benunit = person.benunit.any(head) + paid_to_head = person.household.sum(paid * ~in_head_benunit) + heads = person.household.sum(head) + return where(head, paid_to_head / max_(heads, 1), 0) diff --git a/policyengine_uk/variables/input/rent_paid_as_boarder.py b/policyengine_uk/variables/input/rent_paid_as_boarder.py new file mode 100644 index 000000000..92ffc11cf --- /dev/null +++ b/policyengine_uk/variables/input/rent_paid_as_boarder.py @@ -0,0 +1,18 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_boarder(Variable): + value_type = float + entity = Person + label = "rent paid to the householder as a boarder" + documentation = ( + "Rent this person pays the householder for board and lodging (a room " + "and at least some meals) in the householder's home, where this " + "person lives as a member of the household outside the householder's " + "benefit unit. The Family Resources Survey records it on the payer " + "(CVPAY where CONVBL is 1). Annual amount; the weekly means-test " + "rules divide it by 52." + ) + definition_period = YEAR + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" diff --git a/policyengine_uk/variables/input/rent_paid_as_lodger.py b/policyengine_uk/variables/input/rent_paid_as_lodger.py new file mode 100644 index 000000000..0859f6b9f --- /dev/null +++ b/policyengine_uk/variables/input/rent_paid_as_lodger.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class rent_paid_as_lodger(Variable): + value_type = float + entity = Person + label = "rent paid to the householder as a lodger" + documentation = ( + "Rent this person pays the householder for lodging without meals in " + "the householder's home, where this person lives as a member of the " + "household outside the householder's benefit unit. The Family " + "Resources Survey records it on the payer (CVPAY where CONVBL is not " + "1). Annual amount; the weekly means-test rules divide it by 52." + ) + definition_period = YEAR + unit = GBP + uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" diff --git a/policyengine_uk/variables/input/sublet_income.py b/policyengine_uk/variables/input/sublet_income.py index f1e600600..daaad5ce3 100644 --- a/policyengine_uk/variables/input/sublet_income.py +++ b/policyengine_uk/variables/input/sublet_income.py @@ -5,7 +5,14 @@ class sublet_income(Variable): value_type = float entity = Person label = "sublet income" - documentation = "Income from subletting properties" + documentation = ( + "Rent this person receives for letting part of the home they live in " + "to someone who is not a member of the household. Income tax treats " + "it as rent-a-room receipts, and the legacy means tests count it less " + "a weekly disregard. Rent from other property is property_income; " + "rent from boarders and lodgers who live in the household is " + "rent_from_boarders_and_lodgers." + ) definition_period = YEAR unit = GBP uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp"