diff --git a/changelog.d/boarder-lodger-rent.added.md b/changelog.d/boarder-lodger-rent.added.md new file mode 100644 index 0000000000..7546d6b98a --- /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 a boarder or lodger and their partner, while a non-paying adult in their family stays a non-dependant. 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 0000000000..4d56006381 --- /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 0000000000..28765f4b56 --- /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 0000000000..887f419028 --- /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 0000000000..de27030513 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers.yaml @@ -0,0 +1,652 @@ +# 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 + # before the winter fuel payment charge, which the means tests never deduct. + # 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_before_winter_fuel_payment_charge: [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)). uc_income_tax_on_earnings takes + # earnings as the lowest slice, so the taxable rent raises + # earned_taxable_income and the other income above earnings alike and the + # tax on earnings stays 1,486: 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_income_tax_on_earnings: [1_486, 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] + +- name: The finance-cost reduction comes off no more than the property tax on income from capital + # 2026-27, England. Private pension 30,000: tax (30,000 - 12,570) x 20% = + # 3,486. Property income 2,000 with finance costs 10,000, and a lodger + # paying 15,000: taxable rent-a-room income 15,000 - 7,500 = 7,500, so + # taxable property income 9,500 and property income tax 1,900, of which + # 7,500 / 9,500 x 1,900 = 1,500 falls on the rent-a-room income. The + # finance-cost reduction is 20% x min(10,000, 9,500) = 1,900. Counted tax + # 3,486 + 1,500 = 4,986; capital tax 1,900 - 1,500 = 400. The reduction + # comes off the capital side's 400 only; the other 1,500 is shared in + # proportion to the Step 4 tax left (4,986 counted, 0 capital). So the + # capital side keeps nil and the tax on counted income is + # 4,986 - 1,500 = 3,486, which is the whole income tax. The rent-a-room + # tax keeps 1,500 - 1,500 x 1,500 / 4,986 = 1,048.74. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 50 + is_household_head: true + private_pension_income: 30_000 + property_income: 2_000 + property_finance_costs: 10_000 + lodger: + age: 30 + is_household_head: false + rent_paid_as_lodger: 15_000 + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [lodger] + households: + household: + members: [householder, lodger] + region: LONDON + output: + taxable_property_income: [9_500, 0] + property_income_tax: [1_900, 0] + rent_a_room_income_tax: [1_500, 0] + property_finance_cost_relief: [1_900, 0] + income_tax_before_winter_fuel_payment_charge: [3_486, 0] + legacy_means_test_income_tax: [3_486, 0] + rent_a_room_income_tax_after_reductions: [1_048.74, 0] + +- name: Taxable rent-a-room income reaches total income and adjusted net income once + # 2026-27, England. Earnings 20,000 and a lodger paying 10,000: taxable + # rent-a-room income 2,500. Total income and adjusted net income both take + # it through taxable_property_income only: 20,000 + 2,500 = 22,500. + # Household post-tax income adds it once through gross_taxable_inputs: + # 22,500 less income tax (1,486 + 500 = 1,986) and NI 594.40 = 19,919.60. + 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: + taxable_rent_a_room_income: [2_500, 0] + taxable_property_income: [2_500, 0] + total_income: [22_500, 0] + adjusted_net_income: [22_500, 0] + gross_taxable_inputs: [22_500, 0] + post_tax_income: 19_919.60 + +- name: CTR on the Universal Credit route with pay deducts the extra tax earnings cause on uncounted rent + # 2026-27, England, working age, with a Universal Credit award (set as an + # input; Universal Credit itself is nil at this income). Pay 30,000: tax + # (30,000 - 12,570) x 20% = 3,486, NI 17,430 x 8% = 1,394.40. A lodger pays + # 15,000: taxable rent-a-room income 7,500, taxed at 20%: 1,500. The route + # does not count the rent. Without the pay, 7,500 falls inside the 12,570 + # personal allowance, so the rent bears no tax and nil comes off; the + # 1,500 that earnings cause stays deducted. Income: 30,000 - (3,486 + + # 1,500) - 1,394.40 = 23,619.60, which is 1,500 below the 25,119.60 + # without the lodger. + 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: 15_000 + benunits: + householder_unit: + members: [householder] + council_tax_reduction_universal_credit_award: true + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [householder, lodger] + region: NORTH_WEST + output: + rent_a_room_income_tax: [1_500, 0] + rent_a_room_income_tax_without_earnings: [0, 0] + legacy_means_test_deducted_income_tax: [4_986, 0] + council_tax_reduction_applicable_income: [23_619.60, 0] + +- name: CTR on the Universal Credit route without pay keeps uncounted rent out of income + # The same household without the pay. Taxable rent-a-room income 7,500 + # falls inside the personal allowance, so there is no tax, and the route + # counts neither the rent nor any tax on it. Income is the Universal Credit + # award alone, the single standard allowance of 424.90 a month: 424.90 x 12 + # = 5,098.80. The general rules would have added + # (15,000 / 52 - 20) x 52 = 13,960. + 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: 15_000 + benunits: + householder_unit: + members: [householder] + council_tax_reduction_universal_credit_award: true + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [householder, lodger] + region: NORTH_WEST + output: + legacy_benefits_home_letting_income: [13_960, 0] + rent_a_room_income_tax_without_earnings: [0, 0] + universal_credit: [5_098.80, 0] + council_tax_reduction_applicable_income: [5_098.80, 0] + +- name: Scottish working-age CTR with pay deducts the extra tax earnings cause on uncounted rent + # 2026-27, Scotland, working age, no Universal Credit. Scottish rates above + # the 12,570 personal allowance: 19% to 3,967, 20% to 16,956, 21% to + # 31,092. Pay 30,000 leaves 17,430 taxable: 3,967 x 19% + 12,989 x 20% + + # 474 x 21% = 753.73 + 2,597.80 + 99.54 = 3,451.07. The rent's 7,500 + # taxable falls at 21%: 1,575. NI 17,430 x 8% = 1,394.40. The scheme does + # not list the rent (SSI 2021/249 reg 57(1)). Without the pay the rent + # bears no tax, so nil comes off; the deduction keeps the rent's whole + # 1,575 at Scottish rates. Income: 30,000 - (3,451.07 + 1,575) - 1,394.40 + # = 23,579.53, which is 1,575 below the 25,154.53 without the lodger. + 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: 15_000 + benunits: + householder_unit: + members: [householder] + would_claim_uc: false + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [householder, lodger] + region: SCOTLAND + output: + earned_income_tax: [3_451.07, 0] + rent_a_room_income_tax: [1_575, 0] + rent_a_room_income_tax_without_earnings: [0, 0] + legacy_means_test_deducted_income_tax: [5_026.07, 0] + council_tax_reduction_applicable_income: [23_579.53, 0] + +- name: Scottish working-age CTR without pay keeps uncounted rent out of income + # The same household without the pay: no tax, and the scheme counts + # neither the rent nor any tax on it: income nil. + 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: 15_000 + benunits: + householder_unit: + members: [householder] + would_claim_uc: false + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [householder, lodger] + region: SCOTLAND + output: + rent_a_room_income_tax_without_earnings: [0, 0] + council_tax_reduction_applicable_income: [0, 0] + +- name: CTR on the Universal Credit route leaves out the tax uncounted rent bears without earnings + # 2026-27, England, working age, with a Universal Credit award (set as an + # input). Pay 30,000 and a lodger paying 30,000: taxable rent-a-room income + # 30,000 - 7,500 = 22,500. With the pay, taxable income 52,500 - 12,570 = + # 39,930: 37,700 x 20% + 2,230 x 40% = 7,540 + 892 = 8,432, of which the + # pay alone bears (30,000 - 12,570) x 20% = 3,486 and the rent 4,946. + # Without the pay, the rent bears (22,500 - 12,570) x 20% = 1,986, which + # the route leaves out of the deducted tax; the other 2,960 is the extra + # tax earnings cause on the rent, and stays deducted. NI 17,430 x 8% = + # 1,394.40. Income: 30,000 - (8,432 - 1,986) - 1,394.40 = 22,159.60. + 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: 30_000 + benunits: + householder_unit: + members: [householder] + council_tax_reduction_universal_credit_award: true + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [householder, lodger] + region: NORTH_WEST + output: + income_tax_before_winter_fuel_payment_charge: [8_432, 0] + rent_a_room_income_tax: [4_946, 0] + rent_a_room_income_tax_without_earnings: [1_986, 0] + legacy_means_test_deducted_income_tax: [8_432, 0] + council_tax_reduction_applicable_income: [22_159.60, 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 0000000000..54a449768d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/boarders_and_lodgers_non_dependants.yaml @@ -0,0 +1,382 @@ +# 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] + +# Person-level liability. HB Regs 2006 reg 8(1)(b) treats the partner of a +# liable person as liable; the model applies that rule to UC and CTR too, as +# a stated assumption. Nobody else in a payer's benefit unit is liable, so a +# lodger's adult son who pays nothing stays the householder's non-dependant. +# The household head's own benefit unit never pays the householder. + +- name: A lodger's non-paying adult son is the householder's non-dependant for Housing Benefit + # Scotland, 2026-27. The household head (60) claims Housing Benefit on an + # eligible rent of 10,400 a year. An unrelated lodger (50) pays 5,200 a year + # (100 a week); her son (30), in her benefit unit, pays nothing and earns + # 15,600 a year (300 a week) over 20 hours a week, with no exemption. + # The lodger is liable on a commercial basis (reg 3(2)(e)(i)); her son is + # not, so he is a non-dependant. He works 20 hours, at least the 16-hour + # remunerative work threshold, so his deduction is banded: 300 a week lies + # in the band from 279 to 365, whose 2026-27 deduction is 64.35 a week + # (non_dep_deduction/amount.yaml): 64.35 x 52 = 3,346.20 a year. + # Counted home-letting income: one lodger family at working age, + # (100 - 20) x 52 = 4,160 a year. The applicable amount, 95.55 x 52 = + # 4,968.60, exceeds it, so Housing Benefit is the eligible rent less the + # deduction: 10,400 - 3,346.20 = 7,053.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + is_claimant_or_partner: true + housing_benefit_reported: 1 + lodger: + age: 50 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + son: + age: 30 + is_household_head: false + # The inference would make an adult of 30 the partner of a claimant + # of 50, so the roles are supplied. + is_claimant_or_partner: false + employment_income: 15_600 + weekly_hours: 20 + benunits: + householder_unit: + members: [householder] + would_claim_uc: false + lodger_unit: + members: [lodger, son] + households: + household: + members: [householder, lodger, son] + rent: 10_400 + tenure_type: RENT_FROM_COUNCIL + region: SCOTLAND + output: + pays_rent_to_householder: [false, true, false] + benunit_is_liable_for_household_rent: [true, false] + is_non_dependant_of_household_head: [false, false, true] + housing_benefit_individual_non_dep_deduction_eligible: [false, false, true] + housing_benefit_non_dep_deductions: [3_346.20, 0] + legacy_benefits_home_letting_income: [4_160, 0] + housing_benefit_applicable_income: [4_160, 0] + housing_benefit: [7_053.80, 0] + +- name: A lodger's non-paying adult son is the householder's non-dependant for Universal Credit and CTR + # The same household, with the household head claiming Universal Credit. + # The son is a non-dependant (Sch 4 para 9(2)(d) excludes only a person + # liable to pay on a commercial basis), aged 21 or over and not exempt, so + # the housing cost contribution is 96.55 a month in 2026-27 + # (elements/housing/non_dep_deduction/amount.yaml): 96.55 x 12 = 1,158.60. + # The housing element is 10,400 - 1,158.60 = 9,241.40. Council tax + # reduction takes the same non-dependant flag and, in the model, the + # Housing Benefit scale: 64.35 x 52 = 3,346.20. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 50 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + son: + age: 30 + is_household_head: false + is_claimant_or_partner: false + employment_income: 15_600 + weekly_hours: 20 + benunits: + householder_unit: + members: [householder] + would_claim_uc: true + lodger_unit: + members: [lodger, son] + households: + household: + members: [householder, lodger, son] + rent: 10_400 + tenure_type: RENT_FROM_COUNCIL + region: SCOTLAND + output: + uc_individual_non_dep_deduction_eligible: [false, false, true] + uc_non_dep_deductions: [1_158.60, 0] + uc_housing_costs_element: [9_241.40, 5_200] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true] + council_tax_reduction_non_dep_deductions: [3_346.20, 0] + +- name: A working-age lodger couple where one pays is one family for the disregard and neither is a non-dependant + # England, 2026-27. One partner pays 6,240 a year (120 a week); the other + # pays nothing. Both are liable: the payer directly, the partner as the + # payer's partner (HB reg 8(1)(b), and the model's assumption for UC and + # CTR). At working age the disregard is 20 on the aggregate paid by the + # occupier and their family (HB Sch 5 para 22): (120 - 20) x 52 = 5,200. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 45 + is_household_head: true + is_claimant_or_partner: true + payer: + age: 30 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 6_240 + partner: + age: 30 + is_household_head: false + is_claimant_or_partner: true + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [payer, partner] + households: + household: + members: [householder, payer, partner] + rent: 9_000 + tenure_type: RENT_PRIVATELY + region: LONDON + output: + pays_rent_to_householder: [false, true, true] + is_non_dependant_of_household_head: [false, false, false] + uc_individual_non_dep_deduction_eligible: [false, false, false] + housing_benefit_individual_non_dep_deduction_eligible: [false, false, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false] + legacy_benefits_home_letting_income: [5_200, 0] + +- name: A lodger couple at pension age where one pays keeps the per-person disregard + # England, 2026-27, householder over State Pension age. The same couple: + # the pension-age rules disregard 20 of "the amount paid by that person" + # (HB (SPC) Sch 5 para 10; SPC Regs Sch IV para 9), so the payer's 120 + # gives (120 - 20) x 52 = 5,200, and the partner, who pays nothing, adds + # no disregard. Neither is a non-dependant. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 70 + is_household_head: true + is_claimant_or_partner: true + payer: + age: 30 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 6_240 + partner: + age: 30 + is_household_head: false + is_claimant_or_partner: true + benunits: + householder_unit: + members: [householder] + lodger_unit: + members: [payer, partner] + households: + household: + members: [householder, payer, partner] + rent: 9_000 + tenure_type: RENT_PRIVATELY + region: LONDON + output: + pays_rent_to_householder: [false, true, true] + is_non_dependant_of_household_head: [false, false, false] + housing_benefit_individual_non_dep_deduction_eligible: [false, false, false] + legacy_benefits_home_letting_income: [5_200, 0] + +- name: A son who pays leaves the count and his non-paying claimant mother stays in it + # Scotland, 2026-27. In the lodger's benefit unit the claimant mother (50) + # pays nothing and earns 15,600 a year over 20 hours a week; her son (25), + # who is not a claimant or partner, pays 5,200 a year as a lodger. A payer + # who is not a claimant or partner makes nobody else liable, so the son + # leaves the count and his mother is a non-dependant: 64.35 x 52 = + # 3,346.20, as in the first case. Counted home-letting income is + # (100 - 20) x 52 = 4,160, and Housing Benefit 10,400 - 3,346.20 = + # 7,053.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 60 + is_household_head: true + is_claimant_or_partner: true + housing_benefit_reported: 1 + mother: + age: 50 + is_household_head: false + is_claimant_or_partner: true + employment_income: 15_600 + weekly_hours: 20 + son: + age: 25 + is_household_head: false + is_claimant_or_partner: false + rent_paid_as_lodger: 5_200 + benunits: + householder_unit: + members: [householder] + would_claim_uc: false + lodger_unit: + members: [mother, son] + households: + household: + members: [householder, mother, son] + rent: 10_400 + tenure_type: RENT_FROM_COUNCIL + region: SCOTLAND + output: + pays_rent_to_householder: [false, false, true] + benunit_is_liable_for_household_rent: [true, false] + is_non_dependant_of_household_head: [false, true, false] + housing_benefit_individual_non_dep_deduction_eligible: [false, true, false] + uc_individual_non_dep_deduction_eligible: [false, true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true, false] + housing_benefit_non_dep_deductions: [3_346.20, 0] + legacy_benefits_home_letting_income: [4_160, 0] + housing_benefit: [7_053.80, 0] + +- name: Rent entered for a member of the household head's benefit unit is not paid to the householder + # The household head's partner has rent_paid_as_lodger set. The household + # head's benefit unit is never liable to the householder, so neither member + # pays rent to the householder and the head receives none. + period: 2026 + input: + people: + householder: + age: 45 + is_household_head: true + is_claimant_or_partner: true + partner: + age: 45 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 4_000 + benunits: + householder_unit: + members: [householder, partner] + households: + household: + members: [householder, partner] + rent: 9_000 + tenure_type: RENT_PRIVATELY + output: + pays_rent_to_householder: [false, false] + is_non_dependant_of_household_head: [false, false] + rent_from_boarders_and_lodgers: [0, 0] + +- name: A lodger's son keeps the Pension Credit exemption from the UC housing cost contribution + # England, 2026-27. The household head (40) claims Universal Credit. The + # lodger (75) pays 5,200 a year and has a State Pension of 6,000 a year; + # her son (40), in her benefit unit, pays nothing and earns 15,600 over 20 + # hours. He is a non-dependant, but no housing cost contribution applies + # where the non-dependant's family receives Pension Credit (UC Regs 2013 + # Sch 4 para 16). Her guarantee credit is the 238 a week standard minimum + # guarantee less her income: 238 x 52 - 6,000 = 6,376 (her son's earnings + # are not hers). uc_non_dep_receives_pension_credit computes it with her + # family's UC housing element at nil; a family over the qualifying age + # claims no UC, so that is her actual award. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 75 + is_household_head: false + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + state_pension: 6_000 + son: + age: 40 + is_household_head: false + is_claimant_or_partner: false + employment_income: 15_600 + weekly_hours: 20 + benunits: + householder_unit: + members: [householder] + would_claim_uc: true + lodger_unit: + members: [lodger, son] + households: + household: + members: [householder, lodger, son] + rent: 10_400 + tenure_type: RENT_FROM_COUNCIL + region: LONDON + output: + is_non_dependant_of_household_head: [false, false, true] + pension_credit: [0, 6_376] + uc_non_dep_receives_pension_credit: [false, true] + uc_individual_non_dep_deduction_eligible: [false, false, false] + uc_non_dep_deductions: [0, 0] 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 0000000000..6160dbe273 --- /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 0000000000..68b38ea818 --- /dev/null +++ b/policyengine_uk/tests/test_boarder_lodger_rent_properties.py @@ -0,0 +1,510 @@ +"""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). A paying benefit unit may also hold a partner or an adult +who pays nothing; the generator supplies each person's role +(is_claimant_or_partner). + +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. Rent a programme does not count: UC earned income does not change with the + rent. On the council tax reduction routes that do not count it (the + Universal Credit route, and the Scottish working-age scheme), income does + not change with the rent where the applicant and partner have no + earnings; with earnings it falls by the extra tax earnings cause on the + rent and by nothing else, so by no more than the top rate on the change in + taxable rent-a-room income. +8. Tax: legacy_means_test_income_tax is between nil and income tax; + rent_a_room_income_tax_after_reductions is between nil and + rent_a_room_income_tax; the tax on income from capital left after + reductions inside legacy_means_test_income_tax never goes negative; and + for a person whose only counted income is rent-a-room income, + rent_a_room_income_tax_after_reductions equals + legacy_means_test_income_tax. + +Person-level liability: + +9. pays_rent_to_householder holds exactly for a person outside the household + head's benefit unit who pays, or who is a claimant or partner in a benefit + unit where a claimant or partner pays. +10. Moving a non-paying adult who is not a claimant or partner out of the + payer's benefit unit into one of their own leaves the household head's + Housing Benefit and council tax reduction non-dependant deductions + unchanged. +11. Adding such an adult never lowers the household head's Housing Benefit + non-dependant deductions, and never raises the head's Housing Benefit + unless it raises the head's bedroom count under the size criteria. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.utils.legacy_means_test_tax import legacy_means_test_tax_split + +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 +TOP_RATE = 0.48 +KINDS = ("rent_paid_as_boarder", "rent_paid_as_lodger") +AWARDS = ["income_support", "housing_benefit", "pension_credit", "council_tax_benefit"] +rent = st.one_of(st.just(0.0), st.floats(100, 20_000)) +positive_rent = st.floats(100, 20_000) + + +@st.composite +def member(draw, claimant_or_partner, pays=None): + kind = draw(st.sampled_from(KINDS)) + amount = draw(rent) if pays is None else (draw(positive_rent) if pays else 0.0) + return { + "age": draw(st.integers(19, 60)), + "is_claimant_or_partner": claimant_or_partner, + "employment_income": draw(st.one_of(st.just(0.0), st.floats(0, 25_000))), + kind: amount, + } + + +@st.composite +def payer_units(draw): + """A benefit unit outside the head's: one or two claimants or partners, + and possibly an adult who is neither, each paying or not.""" + unit = [draw(member(True)) for _ in range(draw(st.integers(1, 2)))] + if draw(st.booleans()): + unit.append(draw(member(False))) + return unit + + +@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))) + ] + return dict( + head_unit=head_unit, + payers=[draw(payer_units()) for _ in range(draw(st.integers(0, 2)))], + 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"])), + claims_housing_benefit=draw(st.booleans()), + ) + + +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}, + "is_claimant_or_partner": {year: True}, + "sublet_income": {year: h["sublet"] if i == 0 else h["partner_sublet"]}, + } + if h.get("claims_housing_benefit") and i == 0: + people[name]["housing_benefit_reported"] = {year: 1.0} + names.append(name) + benunits["head_unit"] = {"members": list(names)} + if h.get("claims_housing_benefit"): + benunits["head_unit"]["would_claim_uc"] = {year: False} + 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}, + "is_claimant_or_partner": {year: attrs["is_claimant_or_partner"]}, + "employment_income": {year: attrs.get("employment_income", 0.0)}, + } + for kind in KINDS: + 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 paid(attrs, kind=None): + if kind: + return attrs.get(kind, 0.0) + return sum(attrs.get(k, 0.0) for k in KINDS) + + +def payments(h, kind=None): + return [[paid(a, kind) for a in unit] for unit in h["payers"]] + + +def expected_pays_rent_to_householder(h): + """The person-level liability rule, from the inputs, in person order.""" + expected = [False] * len(h["head_unit"]) + for unit in h["payers"]: + partner_pays = any(a["is_claimant_or_partner"] and paid(a) > 0 for a in unit) + expected += [ + paid(a) > 0 or (a["is_claimant_or_partner"] and partner_pays) for a in unit + ] + return np.array(expected) + + +def head_taxable_rent_a_room(h, scale=1.0): + """Taxable rent-a-room income of the head's benefit unit, from the inputs.""" + head = sum(sum(unit) for unit in payments(h)) * scale + h["sublet"] + receipts = [head] + if len(h["head_unit"]) > 1: + receipts.append(h["partner_sublet"]) + shared = sum(r > 0 for r in receipts) > 1 + limit = BASIC_AMOUNT / 2 if shared else BASIC_AMOUNT + return sum(max(0, r - limit) for r in receipts) + + +@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"] + total = sum(sum(unit) for unit in payments(h)) + np.testing.assert_allclose(received.sum(), total, 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_tax_bounds(h, year): + sim = Simulation(situation=situation(h, year)) + v = { + name: np.asarray(sim.calculate(name, year), dtype=float) + for name in [ + "legacy_means_test_income_tax", + "income_tax", + "rent_a_room_income_tax", + "rent_a_room_income_tax_after_reductions", + ] + } + assert np.all(v["legacy_means_test_income_tax"] >= 0) + assert np.all(v["legacy_means_test_income_tax"] <= v["income_tax"] + 0.01) + after = v["rent_a_room_income_tax_after_reductions"] + assert np.all(after >= -0.01) + assert np.all(after <= v["rent_a_room_income_tax"] + 0.01) + split = legacy_means_test_tax_split(sim.populations["person"], str(year)) + assert np.all(split["capital_after_reductions"] >= -0.01) + + +@st.composite +def rent_a_room_only(draw): + """A household head whose only counted income is rent-a-room income, with + income from capital, finance costs and a Step 6 reduction.""" + return dict( + rent=draw(st.floats(0, 60_000)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + dividends=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + property=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + finance_costs=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + other_tax_credits=draw(st.one_of(st.just(0.0), st.floats(0, 5_000))), + region=draw(st.sampled_from(["LONDON", "SCOTLAND"])), + ) + + +@PROPERTY_SETTINGS +@given(rent_a_room_only(), st.sampled_from(YEARS)) +def test_rent_a_room_tax_after_reductions_matches_the_counted_tax(h, year): + situation = { + "people": { + "head": { + "age": {year: 45}, + "is_household_head": {year: True}, + "savings_interest_income": {year: h["savings"]}, + "dividend_income": {year: h["dividends"]}, + "property_income": {year: h["property"]}, + "property_finance_costs": {year: h["finance_costs"]}, + "other_tax_credits": {year: h["other_tax_credits"]}, + }, + "lodger": { + "age": {year: 30}, + "is_household_head": {year: False}, + "rent_paid_as_lodger": {year: h["rent"]}, + }, + }, + "benunits": { + "head_unit": {"members": ["head"]}, + "lodger": {"members": ["lodger"]}, + }, + "households": { + "household": {"members": ["head", "lodger"], "region": {year: h["region"]}} + }, + } + sim = Simulation(situation=situation) + counted = sim.calculate("legacy_means_test_income_tax", year)[0] + rent_a_room = sim.calculate("rent_a_room_income_tax_after_reductions", year)[0] + np.testing.assert_allclose(rent_a_room, counted, rtol=1e-5, atol=0.01) + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS), st.floats(0.3, 0.9)) +def test_programmes_that_exclude_the_rent_deduct_only_the_tax_earnings_cause( + h, year, scale +): + # Both runs keep every payment positive, so non-dependant status is fixed. + variables = [ + "uc_earned_income", + "council_tax_reduction_applicable_income", + "council_tax_reduction_universal_credit_award", + "council_tax_reduction_pensioner", + ] + 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["council_tax_reduction_pensioner"][head_unit] + uc_route = working_age and bool( + full["council_tax_reduction_universal_credit_award"][head_unit] + ) + same_route = ( + full["council_tax_reduction_universal_credit_award"][head_unit] + == part["council_tax_reduction_universal_credit_award"][head_unit] + ) + excluded = uc_route or (h["region"] == "SCOTLAND" and working_age) + if not (excluded and same_route): + return + fall = ( + part["council_tax_reduction_applicable_income"][head_unit] + - full["council_tax_reduction_applicable_income"][head_unit] + ) + earnings = sum(attrs["employment_income"] for attrs in h["head_unit"]) + if earnings == 0: + np.testing.assert_allclose(fall, 0, atol=0.01) + else: + more_taxable = head_taxable_rent_a_room(h) - head_taxable_rent_a_room(h, scale) + assert -0.01 <= fall <= TOP_RATE * more_taxable + 0.01 + + +# Person-level liability. + + +@PROPERTY_SETTINGS +@given(households(), st.sampled_from(YEARS)) +def test_pays_rent_to_householder_is_person_level(h, year): + v = calculate(h, year, ["pays_rent_to_householder"]) + np.testing.assert_array_equal( + v["pays_rent_to_householder"].astype(bool), + expected_pays_rent_to_householder(h), + ) + + +@st.composite +def households_with_a_non_paying_adult(draw): + """A household whose first paying unit has a payer who is a claimant, and + an adult who is neither claimant nor partner and pays nothing.""" + h = draw(households()) + payer = draw(member(True, pays=True)) + adult = draw(member(False, pays=False)) + adult["employment_income"] = draw(st.floats(0, 40_000)) + h["payers"] = [[payer], *h["payers"]] + h["tenure"] = "RENT_PRIVATELY" + return h, adult + + +def with_adult(h, adult, own_unit): + h = dict(h, payers=[list(unit) for unit in h["payers"]]) + if own_unit: + h["payers"].append([dict(adult, is_claimant_or_partner=True)]) + else: + h["payers"][0].append(adult) + return h + + +@PROPERTY_SETTINGS +@given(households_with_a_non_paying_adult(), st.sampled_from(YEARS)) +def test_a_non_paying_adult_counts_the_same_in_or_out_of_the_payers_unit(case, year): + h, adult = case + variables = [ + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + ] + inside = calculate(with_adult(h, adult, own_unit=False), year, variables) + outside = calculate(with_adult(h, adult, own_unit=True), year, variables) + for v in variables: + np.testing.assert_allclose(inside[v][0], outside[v][0], atol=0.01, err_msg=v) + + +@PROPERTY_SETTINGS +@given(households_with_a_non_paying_adult(), st.sampled_from(YEARS)) +def test_adding_a_non_paying_adult_never_raises_housing_benefit(case, year): + # A non-dependant also occupies a bedroom under the size criteria (HB + # Regs 2006 reg 13D(12)), which can lift the maximum rent, for example + # off the shared accommodation rate. So the property holds where the + # household head's bedroom count does not change, and the non-dependant + # deductions never fall. + h, adult = case + h = dict(h, claims_housing_benefit=True) + variables = [ + "housing_benefit", + "housing_benefit_LHA_allowed_bedrooms", + "housing_benefit_non_dep_deductions", + ] + without = calculate(h, year, variables) + with_them = calculate(with_adult(h, adult, own_unit=False), year, variables) + assert ( + with_them["housing_benefit_non_dep_deductions"][0] + >= without["housing_benefit_non_dep_deductions"][0] - 0.01 + ) + same_rooms = ( + with_them["housing_benefit_LHA_allowed_bedrooms"][0] + == without["housing_benefit_LHA_allowed_bedrooms"][0] + ) + if same_rooms: + assert with_them["housing_benefit"][0] <= without["housing_benefit"][0] + 0.01 diff --git a/policyengine_uk/tests/test_household_head_agreement_properties.py b/policyengine_uk/tests/test_household_head_agreement_properties.py index 4862578ad5..a59afeefe9 100644 --- a/policyengine_uk/tests/test_household_head_agreement_properties.py +++ b/policyengine_uk/tests/test_household_head_agreement_properties.py @@ -390,7 +390,24 @@ def test_every_programme_reads_one_head(population): # Non-dependants (pin). non_dependant = calc(sim, "is_non_dependant_of_household_head") rent_liable = calc(sim, "benunit_is_rent_liable") - assert np.array_equal(non_dependant, ~person_head_family & ~rent_liable[benunit_of]) + # Liability to the householder is person by person: the payer, and a + # claimant or partner whose family's claimant or partner pays. A family + # whose only rent is that payment is not liable for the household's rent, + # so its other members can be non-dependants. + pays = paid_to_householder > 0 + claimant_or_partner = calc(sim, "is_claimant_or_partner") + partner_pays = any_in_benunit(pays & claimant_or_partner, facts)[benunit_of] + liable_to_householder = ~person_head_family & ( + pays | (claimant_or_partner & partner_pays) + ) + family_pays_householder = any_in_benunit(liable_to_householder, facts) + liable_for_household_rent = rent_liable & ~family_pays_householder + assert np.array_equal( + non_dependant, + ~person_head_family + & ~liable_for_household_rent[benunit_of] + & ~liable_to_householder, + ) # Universal Credit non-dependant deductions fall on the one claim that # counts the household's non-dependants: the head's family's wherever it # is liable for the rent and claims. diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 0b68d8b3b1..2bf3482266 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -259,7 +259,24 @@ def test_non_dependants(population): payer = np.isin(roles, ["boarder", "lodger"]) liable_for_rent = calc(sim, "benunit_is_rent_liable") non_dependant = calc(sim, "is_non_dependant_of_household_head") - person_liable = sim.map_result(liable_for_rent, "benunit", "person") > 0 + # Liability to the householder is person by person: a boarder's or + # lodger's non-paying child or adult relative can be a non-dependant, + # while the payer and the payer's partner cannot. + pays = (calc(sim, "rent_paid_as_boarder") + calc(sim, "rent_paid_as_lodger")) > 0 + claimant_or_partner = calc(sim, "is_claimant_or_partner") + partner_pays = ( + sim.map_result( + sim.map_result(pays & claimant_or_partner, "person", "benunit") > 0, + "benunit", + "person", + ) + > 0 + ) + liable_to_householder = pays | (claimant_or_partner & partner_pays) + family_pays = np.isin(roles, ["boarder", "lodger"]) + person_liable = ( + sim.map_result(liable_for_rent & ~family_pays, "benunit", "person") > 0 + ) | liable_to_householder assert not np.any(non_dependant & person_liable) assert non_dependant.any() # Universal Credit: one claim per household counts the non-dependants. diff --git a/policyengine_uk/utils/legacy_means_test_tax.py b/policyengine_uk/utils/legacy_means_test_tax.py new file mode 100644 index 0000000000..2212e8d9e3 --- /dev/null +++ b/policyengine_uk/utils/legacy_means_test_tax.py @@ -0,0 +1,63 @@ +"""How the legacy means tests split a person's Step 4 tax and Step 6 reductions. + +``legacy_means_test_income_tax`` and ``rent_a_room_income_tax_after_reductions`` +both read this split, so the two cannot share the reductions differently. +""" + +import numpy as np + +from policyengine_uk.model_api import add, max_, min_ + + +def legacy_means_test_tax_split(person, period): + """Split the Step 4 tax and Step 6 reductions between counted income and + income from capital. + + Counted income carries the tax on earnings, pensions and taxable benefits + (``earned_income_tax``) and the tax on taxable rent-a-room income + (``rent_a_room_income_tax``). Income from capital carries the tax on + savings, dividends and the rest of property income. The residential + finance-cost reduction comes off the tax on property income left on the + capital side, and no further: any excess joins the general reductions, + which the formula shares in proportion to the Step 4 tax left on each side. + """ + rent_a_room_tax = person("rent_a_room_income_tax", period) + property_tax = person("property_income_tax", period) + counted = person("earned_income_tax", period) + rent_a_room_tax + capital_property_tax = max_(0, property_tax - rent_a_room_tax) + capital = ( + add(person, period, ["savings_income_tax", "dividend_income_tax"]) + + capital_property_tax + ) + before_reductions = counted + capital + reductions = min_(person("income_tax_reductions", period), before_reductions) + finance_cost_relief = min_( + person("property_finance_cost_relief", period), + min_(capital_property_tax, reductions), + ) + general_reductions = reductions - finance_cost_relief + capital_after_relief = capital - finance_cost_relief + tax_after_relief = before_reductions - finance_cost_relief + + def share(tax): + return np.divide( + tax, + tax_after_relief, + out=np.zeros_like(tax_after_relief, dtype=float), + where=tax_after_relief > 0, + ) + + return dict( + rent_a_room_tax=rent_a_room_tax, + counted=counted, + capital=capital, + before_reductions=before_reductions, + reductions=reductions, + finance_cost_relief=finance_cost_relief, + general_reductions=general_reductions, + capital_after_relief=capital_after_relief, + capital_after_reductions=capital_after_relief + - general_reductions * share(capital_after_relief), + rent_a_room_after_reductions=rent_a_room_tax + - general_reductions * share(rent_a_room_tax), + ) 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 37ea75a311..c77c460ef1 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,15 @@ 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) + rent_liable = person.benunit("benunit_is_liable_for_household_rent", 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, and their partner (reg 8(1)(b)), is liable to + # pay the claimant on a commercial basis for their occupation, so is + # not a non-dependant (reg 3(2)(e)(i)). A member of their family who + # is not liable stays a non-dependant. + commercial = person("pays_rent_to_householder", period) + return age_eligible & ~rent_liable & ~commercial diff --git a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py index 109ede8fe2..861c4e5350 100644 --- a/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py +++ b/policyengine_uk/variables/gov/dwp/is_non_dependant_of_household_head.py @@ -12,7 +12,9 @@ class is_non_dependant_of_household_head(Variable): "sharer of the rent, a boarder and a lodger are liable on a " "commercial basis for their occupation, so none is a non-dependant " "of anyone, and the household head's household is not a boarder's or " - "lodger's. Universal Credit counts a non-dependant in one claim only " + "lodger's. In a boarder's or lodger's family, only the payer and the " + "payer's partner are liable (pays_rent_to_householder); an adult son " + "or daughter who pays nothing is a non-dependant. Universal Credit counts a non-dependant in one claim only " "(uc_non_dependants_counted); Housing Benefit and Council Tax " "Reduction apportion a non-dependant of several joint occupiers " "between them. Foster children and carers engaged through a charity " @@ -29,5 +31,6 @@ def formula(person, period, parameters): # UC Regs 2013 Sch 4 para 9(2)(d)-(f); HB Regs 2006 reg 3(2)(d)-(e) # and 3(4); HB (SPC) Regs 2006 reg 3. head_family = person.benunit("benunit_contains_household_head", period) - liable_for_rent = person.benunit("benunit_is_rent_liable", period) - return ~head_family & ~liable_for_rent + liable_for_rent = person.benunit("benunit_is_liable_for_household_rent", period) + pays_householder = person("pays_rent_to_householder", period) + return ~head_family & ~liable_for_rent & ~pays_householder 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 2cf125e315..5a45ab690a 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,59 @@ 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 household + # 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_resolved_household_head", period) + pays_head = ~person.benunit("benunit_contains_household_head", period) + 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)), + ) + # Each household has one resolved household head. + head_share = benunit.any(head & claimant_or_partner) + 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 906eab1282..4cad58cb9c 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 @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.legacy_means_test_tax import legacy_means_test_tax_split class legacy_means_test_income_tax(Variable): @@ -17,8 +18,13 @@ class legacy_means_test_income_tax(Variable): "out. Income Support, Housing Benefit, Pension Credit and council tax " "reduction treat savings interest, dividends and property income as " "capital, not income, so the counted income here is the person's " - "other income (earned_income_tax, which covers earnings, pensions and " - "taxable benefits). Its share of the Step 6 reductions (the married " + "other income: earned_income_tax, which covers earnings, pensions and " + "taxable benefits, and rent_a_room_income_tax, the tax on taxable " + "rent from boarders, lodgers and sub-tenants in the home. The tests " + "count that rent as income (HB Regs 2006 Sch 5 paras 22 and 42; HB " + "(SPC) Regs 2006 reg 29(1)(p) and (v); SPC Regs 2002 reg 15(5)(e) and " + "(i)), so its tax moves from the property income tax on the capital " + "side to the counted side. Its share of the Step 6 reductions (the married " "couple's allowance, other tax credits, the residential finance-cost " "reduction and the Pension Tax Allowance) follows two rules. Most " "reductions are not tied to any income: s.27 orders them only to " @@ -30,7 +36,11 @@ class legacy_means_test_income_tax(Variable): "amounts and within the adjusted total income limit (Income Tax " "(Trading and Other Income) Act 2005 ss.274A and 274AA). The " "finance-cost reduction (property_finance_cost_relief) therefore " - "comes off the tax on property income first. The model's " + "comes off the tax on property income first. As a stated convention, " + "it comes off only the property income tax left on the capital side " + "(property_income_tax less rent_a_room_income_tax), so the capital " + "side never goes negative; any excess joins the reductions shared in " + "proportion to the Step 4 tax. The model's " "other_tax_credits input mixes foreign tax credit relief with other " "reductions (venture capital trust, enterprise investment and " "maintenance relief, among others) and records no source, so those " @@ -62,49 +72,23 @@ class legacy_means_test_income_tax(Variable): "https://www.legislation.gov.uk/ukpga/2010/8/section/36", "https://www.legislation.gov.uk/ukpga/2005/5/section/274A", "https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/22", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/42", ] def formula(person, period, parameters): - # 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"], - ) - before_reductions = counted + capital + # Step 4 tax and Step 6 reductions, split between counted income and + # income from capital (see legacy_means_test_tax_split). + split = legacy_means_test_tax_split(person, period) # Step 7 charges other than the winter fuel payment charge, which the # starting value below already leaves out. charges = person("income_tax_charges", period) - # Step 6 reductions, limited to the Step 5 tax. The residential - # finance-cost reduction is relief for the property business's costs, - # given on at most its profits (ITTOIA 2005 ss.274A-274AA), so it - # comes off the tax on property income first. The rest have no - # recorded source and are shared in proportion to the Step 4 tax left. - reductions = min_(person("income_tax_reductions", period), before_reductions) - finance_cost_relief = min_( - person("property_finance_cost_relief", period), - min_(person("property_income_tax", period), reductions), - ) - general_reductions = reductions - finance_cost_relief - capital_after_relief = capital - finance_cost_relief - tax_after_relief = before_reductions - finance_cost_relief - capital_share = np.divide( - capital_after_relief, - tax_after_relief, - out=np.zeros_like(capital_after_relief, dtype=float), - where=tax_after_relief > 0, - ) - capital_after_reductions = ( - capital_after_relief - general_reductions * capital_share - ) # Starting from the income tax the means tests deduct keeps any # supplied value: the result is the tax on counted income less its # share of reductions whenever that tax is calculated. return max_( 0, person("income_tax_before_winter_fuel_payment_charge", period) - - capital_after_reductions + - split["capital_after_reductions"] - charges, ) diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py index b982e91bd2..e3b0690747 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py @@ -16,7 +16,10 @@ class legacy_means_test_uncounted_income_tax_without_earnings(Variable): "income and statutory sick, maternity and paternity pay) or " "self-employment income, that is not tax on income the legacy means " "tests take into account: the tax on rent from property, interest and " - "dividends, and the other Step 7 charges. It is that pre-charge tax " + "dividends, and the other Step 7 charges. Rent from boarders, lodgers " + "and sub-tenants in the home falls outside that list: the tests count " + "it, so legacy_means_test_income_tax holds its tax " + "(rent_a_room_income_tax). It is that pre-charge tax " "less legacy_means_test_income_tax, both on the branch without " "earnings, floored at nil. Income Support, Housing Benefit, Pension " "Credit and council tax reduction treat income derived from capital " diff --git a/policyengine_uk/variables/gov/dwp/rent_a_room_income_tax_without_earnings.py b/policyengine_uk/variables/gov/dwp/rent_a_room_income_tax_without_earnings.py new file mode 100644 index 0000000000..2ac54725be --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/rent_a_room_income_tax_without_earnings.py @@ -0,0 +1,52 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.pension_credit import employed_earnings_sources +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit_earnings_income_tax import ( + DECISION, + person_variable_with_inputs_zeroed, +) + + +class rent_a_room_income_tax_without_earnings(Variable): + value_type = float + entity = Person + label = "Income tax on rent-a-room income, without earnings" + documentation = ( + "rent_a_room_income_tax_after_reductions if nobody in the simulation " + "had employed earnings (employment income and statutory sick, " + "maternity and paternity pay) or self-employment income. Where a " + "council tax reduction scheme does not count rent from boarders, " + "lodgers and sub-tenants in the home (the Universal Credit route, and " + "the Scottish working-age scheme while it does not list that income), " + "council_tax_reduction_applicable_income treats the rent as uncounted " + "income, as legacy_means_test_uncounted_income_tax_without_earnings " + "treats income from capital: it leaves out of the deducted tax the " + "tax this rent would bear without earnings, and keeps the extra tax " + "that earnings cause on it. " + DECISION + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/2007/3/section/23", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/9", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/37", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/57", + ) + + def formula(person, period, parameters): + # The same world without earnings as + # legacy_means_test_uncounted_income_tax_without_earnings. + # Taxable rent-a-room income does not depend on earnings, so without + # it there is no tax on it on any branch. + if not (person("taxable_rent_a_room_income", period) > 0).any(): + return person.empty_array() + sources = employed_earnings_sources(parameters, period) + [ + "self_employment_income" + ] + if not any((person(source, period) != 0).any() for source in sources): + return person("rent_a_room_income_tax_after_reductions", period) + return person_variable_with_inputs_zeroed( + person.simulation, + period, + sources, + "rent_a_room_income_tax_after_reductions", + ) diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py index 801ce96fbf..63b8eb4960 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_current_year_income.py @@ -10,7 +10,10 @@ # 579A) or, for a foreign pension, foreign income (reg 12), which is also step # one. savings_interest_income and dividend_income are investment income (reg # 10(1)(a), (d)), and property_income_after_finance_costs retains the -# model's deduction of property finance costs. +# model's deduction of property finance costs. Property income disregards +# profits sheltered by rent-a-room relief (reg 11(2)), so of rent from +# boarders, lodgers and sub-tenants in the home only taxable_rent_a_room_income +# counts. STEP_ONE_INCOME = [ "state_pension", "widowed_parents_allowance", @@ -18,6 +21,7 @@ "savings_interest_income", "dividend_income", "property_income_after_finance_costs", + "taxable_rent_a_room_income", ] # Social security income (reg 7): benefits payable under the Contributions and 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 abd9869256..49242222e8 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,13 +6,21 @@ 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) + not_rent_liable = ~person.benunit( + "benunit_is_liable_for_household_rent", period + ) p = parameters( period ).gov.dwp.universal_credit.elements.housing.non_dep_deduction 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)). The + # model treats their partner as liable too (pays_rent_to_householder); + # anyone else in their family who pays nothing stays a non-dependant. + 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/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py index d04efdbf02..37a47aab42 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py @@ -15,8 +15,17 @@ class uc_non_dep_receives_pension_credit(Variable): "therefore preserves its entitlement, including legacy-closure " "routing, take-up, explicit inputs and frozen Pension Credit reforms. " "It avoids evaluating a renter's own Pension Credit while calculating " - "their Universal Credit housing element. Rent-liable families' " - "branch results are disregarded because they are not non-dependants." + "their Universal Credit housing element. Results for families liable " + "for the household's rent (benunit_is_liable_for_household_rent) are " + "disregarded because none of their members is a non-dependant. A " + "boarder's or lodger's family is not ruled out: its non-paying adult " + "members can be non-dependants, and keep the exemption when the " + "family receives Pension Credit. For that family the branch fixes " + "the family's Universal Credit housing element, which may cover the " + "rent it pays the householder, at nil, and computes its Pension " + "Credit there. A family whose members are all over the qualifying " + "age claims no Universal " + "Credit, so the branch gives its actual Pension Credit." ) definition_period = YEAR reference = ( @@ -49,4 +58,9 @@ def formula(benunit, period, parameters): finally: # A later period or changed input must start from current inputs. del simulation.branches[name] - return (pension_credit > 0) & ~benunit("benunit_is_rent_liable", period) + # A boarder's or lodger's family still has a non-paying member who can + # be a non-dependant, so only liability for the household's rent + # rules the family out here. + return (pension_credit > 0) & ~benunit( + "benunit_is_liable_for_household_rent", period + ) 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 cac53280c7..692629becc 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 @@ -8,7 +8,10 @@ class taxable_property_income(Variable): 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." + "deduction, floored at nil. The formula adds 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 = [ @@ -20,6 +23,10 @@ class taxable_property_income(Variable): 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. 783BB(2)", + href="https://www.legislation.gov.uk/ukpga/2005/5/section/783BB", + ), ] unit = GBP @@ -29,4 +36,4 @@ def formula(person, period, parameters): person("property_income", period) - person("deductible_property_finance_costs", 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 0000000000..d8c3b0d72f --- /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 0000000000..0ebc0cd093 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/income_tax/rent_a_room/rent_a_room_income_tax_after_reductions.py @@ -0,0 +1,31 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.legacy_means_test_tax import legacy_means_test_tax_split + + +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 (income_tax_reductions). The formula shares the " + "reductions as legacy_means_test_income_tax does: the residential " + "finance-cost reduction comes off the property income tax on income " + "from capital first, and the formula shares the rest in proportion " + "to the Step 4 tax left on each kind of income. For a person whose " + "only counted income is taxable rent-a-room income, the two give the " + "same figure. rent_a_room_income_tax_without_earnings reads it to " + "find the tax on rent that a council tax reduction scheme does not " + "count." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ukpga/2007/3/section/23", + "https://www.legislation.gov.uk/ukpga/2007/3/section/27", + ] + + def formula(person, period, parameters): + return legacy_means_test_tax_split(person, period)[ + "rent_a_room_after_reductions" + ] 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 0000000000..06f51f6965 --- /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 0000000000..d01c781e2f --- /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 0000000000..bb2a8c263c --- /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 0000000000..b212e2e4d1 --- /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/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 96b9b22eef..880dd2ec38 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 @@ -60,7 +60,10 @@ class council_tax_reduction_applicable_income(Variable): "whose income the schemes take from the Secretary of State's " "Universal Credit assessment (which does not count it), or in the " "Scottish scheme for people under pension age from April 2022, which " - "does not list it." + "does not list it. There the rent is uncounted income like income " + "from capital: the tax deducted leaves out the tax the rent would " + "bear without earnings (rent_a_room_income_tax_without_earnings) and " + "keeps the extra tax earnings cause on it." ) definition_period = YEAR unit = GBP @@ -355,6 +358,16 @@ def formula(benunit, period, parameters): ["legacy_means_test_deducted_income_tax", "national_insurance"], members, ) + # Where the scheme does not count the rent, it is uncounted income + # like income from capital: the tax it would bear without earnings + # is not deducted, and the extra tax earnings cause on it is. + tax -= where( + counts_home_letting, + 0, + add_for_members( + benunit, period, ["rent_a_room_income_tax_without_earnings"], members + ), + ) # SI 2012/2885 Sch 1 para 17(9) and Sch 4; WSI 2013/3029 Sch 1 para # 11(9) and Sch 3; SSI 2012/319 reg 31(8) and Sch 2: the pensioner # earnings disregards, capped at net earnings. Zero for working-age diff --git a/policyengine_uk/variables/household/consumption/rent/benunit_is_liable_for_household_rent.py b/policyengine_uk/variables/household/consumption/rent/benunit_is_liable_for_household_rent.py new file mode 100644 index 0000000000..165196a5ec --- /dev/null +++ b/policyengine_uk/variables/household/consumption/rent/benunit_is_liable_for_household_rent.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class benunit_is_liable_for_household_rent(Variable): + value_type = bool + entity = BenUnit + label = "Liable for the household's rent" + documentation = ( + "Whether this family is liable for rent (benunit_is_rent_liable) " + "other than as boarders or lodgers paying the householder. " + "benunit_rent includes what members pay the householder, so a family " + "whose only liability is that payment reads as rent-liable there; " + "here it does not, and each member's own liability is " + "pays_rent_to_householder. The non-dependant rules read this flag: " + "a member of a liable family is not a non-dependant, while in a " + "boarder's or lodger's family only those liable to pay the " + "householder leave the count." + ) + 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", + ) + + def formula(benunit, period, parameters): + pays_householder = benunit.any( + benunit.members("pays_rent_to_householder", period) + ) + return benunit("benunit_is_rent_liable", period) & ~pays_householder diff --git a/policyengine_uk/variables/household/income/gross_taxable_inputs.py b/policyengine_uk/variables/household/income/gross_taxable_inputs.py index 97c3ee9f31..4c959376e8 100644 --- a/policyengine_uk/variables/household/income/gross_taxable_inputs.py +++ b/policyengine_uk/variables/household/income/gross_taxable_inputs.py @@ -19,7 +19,7 @@ class gross_taxable_inputs(Variable): "The historical total_income input sum, retained explicitly for " "post_tax_income and UBI phase-outs. Includes gross wages, private " "pensions, the tax model's social security income, trading and " - "property income, savings and other investment income, dividends " + "property income, taxable rent-a-room income, savings and other investment income, dividends " "and non-negative miscellaneous income. Excludes statutory pay, " "maintenance and tax-exempt benefits; it is not ITA 2007 s.23 total " "income or programme-specific gross income." @@ -33,6 +33,7 @@ class gross_taxable_inputs(Variable): "social_security_income", "self_employment_income", "property_income", + "taxable_rent_a_room_income", "savings_interest_income", "other_investment_income", "dividend_income", 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 3942971ffb..734f3d6b91 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -18,6 +18,7 @@ # property, reported after the mortgage payments and loan interest on # show card K6. property_income is before those finance costs. "property_income_after_finance_costs", + "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 766b11e901..4ded1f1f72 100644 --- a/policyengine_uk/variables/household/income/household_market_income.py +++ b/policyengine_uk/variables/household/income/household_market_income.py @@ -16,6 +16,7 @@ "miscellaneous_income", # Landlords' profit after mortgage interest, as in HBAI income. "property_income_after_finance_costs", + "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 01aa0c71a0..a88c55fb3f 100644 --- a/policyengine_uk/variables/household/income/market_income.py +++ b/policyengine_uk/variables/household/income/market_income.py @@ -25,6 +25,7 @@ def formula(person, period, parameters): "miscellaneous_income", # Landlords' profit after mortgage interest, as in HBAI income. "property_income_after_finance_costs", + "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 index f21363c374..a616a4f759 100644 --- a/policyengine_uk/variables/household/income/pays_rent_to_householder.py +++ b/policyengine_uk/variables/household/income/pays_rent_to_householder.py @@ -4,22 +4,42 @@ class pays_rent_to_householder(Variable): value_type = bool entity = Person - label = "in a family that pays the householder rent as boarders or lodgers" + label = "liable to pay the householder rent as a boarder or lodger" 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." + "Whether this person, outside the household head's benefit unit, is " + "liable to pay the household head on a commercial basis for their " + "occupation as a boarder or lodger, so is not the householder's " + "non-dependant. That covers a person who pays rent for board and " + "lodging or for lodging (rent_paid_as_boarder or rent_paid_as_lodger " + "above nil), and the claimant or partner of a benefit unit where a " + "claimant or partner pays. HB Regs 2006 and HB (SPC) Regs 2006 reg " + "8(1)(b) treat the partner of a liable person as liable. The Universal " + "Credit and council tax reduction rules have no such words; the data " + "hold no agreement, and the model assumes, for every scheme, that a " + "couple's room binds both members. Nobody else in the payer's benefit " + "unit is liable: a payer who is not a claimant or partner, such as a " + "qualifying young person, makes nobody else liable, and a payer's " + "adult son or daughter who pays nothing stays a non-dependant. 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/2006/213/regulation/8", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/8", "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"]) + pays = paid > 0 + claimant_or_partner = person("is_claimant_or_partner", period) + # HB Regs 2006 reg 8(1)(b): the partner of a liable person is liable. + partner_pays = person.benunit.any(pays & claimant_or_partner) in_head_benunit = person.benunit("benunit_contains_household_head", period) - return person.benunit.any(paid > 0) & ~in_head_benunit + return ~in_head_benunit & (pays | (claimant_or_partner & partner_pays)) 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 0000000000..ae5dcc3713 --- /dev/null +++ b/policyengine_uk/variables/household/income/rent_from_boarders_and_lodgers.py @@ -0,0 +1,25 @@ +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 (is_resolved_household_head) receives from " + "boarders and lodgers: the rent paid as a boarder or lodger by " + "household members outside the head's benefit unit. 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_resolved_household_head", period) + in_head_benunit = person.benunit("benunit_contains_household_head", period) + paid_to_head = person.household.sum(paid * ~in_head_benunit) + return where(head, paid_to_head, 0) diff --git a/policyengine_uk/variables/input/rent_paid_as_boarder.py b/policyengine_uk/variables/input/rent_paid_as_boarder.py index ba168a0ce0..92ffc11cf3 100644 --- a/policyengine_uk/variables/input/rent_paid_as_boarder.py +++ b/policyengine_uk/variables/input/rent_paid_as_boarder.py @@ -10,7 +10,8 @@ class rent_paid_as_boarder(Variable): "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)." + "(CVPAY where CONVBL is 1). Annual amount; the weekly means-test " + "rules divide it by 52." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/input/rent_paid_as_lodger.py b/policyengine_uk/variables/input/rent_paid_as_lodger.py index f788d94414..0859f6b9fd 100644 --- a/policyengine_uk/variables/input/rent_paid_as_lodger.py +++ b/policyengine_uk/variables/input/rent_paid_as_lodger.py @@ -10,7 +10,7 @@ class rent_paid_as_lodger(Variable): "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)." + "1). Annual amount; the weekly means-test rules divide it by 52." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/input/sublet_income.py b/policyengine_uk/variables/input/sublet_income.py index f1e600600a..daaad5ce35 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"