diff --git a/changelog.d/721-director-2022.fixed.md b/changelog.d/721-director-2022.fixed.md new file mode 100644 index 0000000000..485e548b1d --- /dev/null +++ b/changelog.d/721-director-2022.fixed.md @@ -0,0 +1 @@ +- Added the 2022–23 annual employee National Insurance schedule for explicitly declared full-year category A directors in one employment, preserving ordinary employees and other years. diff --git a/docs/book/programs/gov/hmrc/national-insurance-directors.md b/docs/book/programs/gov/hmrc/national-insurance-directors.md index 1f0f8b1281..0eb5db5cbf 100644 --- a/docs/book/programs/gov/hmrc/national-insurance-directors.md +++ b/docs/book/programs/gov/hmrc/national-insurance-directors.md @@ -1,106 +1,100 @@ -# Company-director NI (annual cumulative basis) - -```{note} -PolicyEngine UK applies a **single annual NI calculation** at the -person level. For most users this matches what HMRC charges company -directors at year end. This page explains the relationship between the -director-specific "annual cumulative" basis HMRC uses and PolicyEngine's -current modelling, and describes what would need to change to expose a -dedicated director-specific path. Tracked under -[#721](https://github.com/PolicyEngine/policyengine-uk/issues/721). +# Company-director National Insurance + +## Supported 2022–23 employee schedule + +For model year `2022`, the explicit Person/year input +`is_full_year_category_a_director` selects the employee primary Class 1 +schedule for a person who was a director throughout the tax year, had one +employment and was in category A throughout. The input defaults to `False`; +the model does not infer it from salary, dividends or company ownership. +The input label names its supported 2022–23 period. In other model years, +even an explicit `True` leaves the ordinary calculation unchanged. + +[Regulation 8(3) of the Social Security (Contributions) Regulations +2001][reg8] sets the earnings period to the year for a director of the +company at the beginning of that year. + +HMRC's [2022–23 employer guidance][rates] gives this annual director schedule: + +| Annual NI earnings | Employee rate | +| --- | ---: | +| Up to £11,908 | 0% | +| Above £11,908 up to £50,270 | 12.73% | +| Above £50,270 | 2.73% | + +For example, £30,000 of annual NI earnings produces £2,303.1116 before +monetary rounding: (£30,000 − £11,908) × 12.73%. The model retains its existing +floating-point representation; this is not a new payroll rounding rule. +The director route uses the existing `ni_class_1_income` earnings definition, +including its statutory-pay components, and the existing final `ni_liable` +age gate. Employer NI, income-tax and other NI formulas and rates remain +unchanged. Their outputs need not all be unchanged: the existing Class 4 annual +maximum uses primary Class 1 contributions, so it can change for a director +who also has trading profits. That is propagation through the existing cap, +not a new Class 4 rule or a certification of all mixed-income cases. + +A policy-test input for this restricted case is: + +```yaml +period: 2022 +input: + age: 40 + employment_income: 30000 + is_full_year_category_a_director: true +output: + ni_class_1_employee: 2303.1116 ``` -## What HMRC's annual cumulative basis is - -Class 1 NI is normally charged on each *earnings period* (typically a -week or a month) — the employer applies the weekly/monthly thresholds -to that pay period's earnings in isolation. Directors are a special -case because their pay is often irregular: a director might take £0 in -salary for eleven months and then a £200,000 bonus in December. Under -period-by-period NI rules, that pattern would generate massive NI in -the bonus month and almost none in the other eleven, even though the -director's annual income is no different from someone earning a steady -salary. - -To prevent this avoidance route, [SSCBA 1992 s.122 and the *Social -Security (Contributions) Regulations 2001*][nim12000] require directors -to use an **annual earnings period** by default: - -1. The director's NI is computed on a **cumulative** year-to-date - basis. -2. The annual thresholds (Primary Threshold, Upper Earnings Limit, - Secondary Threshold) are applied to YTD earnings, not to each pay - period's earnings in isolation. -3. NI is charged once YTD earnings cross each threshold, and never - refunded if earnings drop later in the year. - -There is also an "**alternative**" method, which approximates the -annual basis with monthly cumulative figures, used for in-year -operational simplicity; the final year-end reconciliation still uses -the annual basis. - -## How PolicyEngine currently handles this - -PolicyEngine UK already computes NI **annually** for every person: - -- The Class 1 employee primary and additional formulas in - [`variables/gov/hmrc/national_insurance/class_1/`](https://github.com/PolicyEngine/policyengine-uk/tree/main/policyengine_uk/variables/gov/hmrc/national_insurance/class_1) - read weekly thresholds from `gov.hmrc.national_insurance.class_1.thresholds` - and convert them to monthly (`× WEEKS_IN_YEAR / MONTHS_IN_YEAR`) before - applying the rate. -- Because the input `employment_income` is annual and is implicitly - spread evenly across months, the **annual NI** the model produces is - identical to the annual cumulative figure a director would owe — for - someone earning the same total over the year, the two methods give - the same answer. - -That's why microsimulation results don't depend on director status. - -## Where the current model could diverge from HMRC's director rules - -Three situations could in principle produce a different answer: - -1. **Irregular within-year pay**. If a household-calculator user inputs - an annual `employment_income` of, say, £80,000 but means it as a - single lump-sum payment, PolicyEngine still spreads it across twelve - months — matching the annual cumulative outcome. There is no - surface here for the user to express the pay timing. -2. **Mid-year start or end** of directorship within a tax year. The - pro-rating rules for directors who start or cease being a director - part-way through a year are slightly different from regular - employees — PolicyEngine assumes a full-year period everywhere. -3. **Multiple employments**. A director with two separate company - roles can sometimes have NI calculated separately per employment. - PolicyEngine has a single `employment_income` per person and so - collapses this case. - -In practice (1) and (2) only matter for the household-calculator -front-end, not microsimulation. (3) is a wider modelling gap that -applies beyond directors. - -## What a dedicated director path would look like - -A future PR addressing this fully would add: - -- A new boolean input `is_company_director` (Person, year). -- A documentation flag on `ni_class_1_employee_primary` / `_additional` - noting that for directors with `is_company_director == true` the - formula returns the same annual figure as the regular path (rather - than period-by-period NI), confirming the equivalence. -- Optional follow-up: a `company_director_pay_timing` enum so that - household-calculator users who model a director's actual pay pattern - can document their intent, but the resulting NI stays on the annual - basis. - -For now PolicyEngine is **functionally correct** for any user whose -input is an annual income — which is the expected interface — and -no code changes are needed in the short term. +The primary and additional component variables allocate their respective +annual liabilities equally over twelve model months. Adding the months +recovers the annual liability. These allocations are **not** statutory +interim payroll deductions, observed monthly cash, cumulative year-to-date +charges or a model of bonus timing. The annual earnings interface does not +represent when pay was received. + +## Why this differs from the ordinary route + +The ordinary employee route reads weekly thresholds and converts them to +monthly equivalents. Existing fiscal parameter processing samples 30 April: +model year `2022` therefore uses the ordinary employee rate and threshold +in force on 30 April 2022. That convention does not reproduce the distinct +annual director schedule above. Annual salary inputs alone do not make the +two routes equivalent for 2022–23. + +The new director parameters store annual thresholds, with raw dates starting +6 April 2022. A separate implementation-coverage parameter is true only for +2022–23, mapping to model year `2022` under that same fiscal convention. +Its false value from 6 April 2023 limits this implementation; it does not +mean that director law ended then. No ordinary rate or threshold history is +changed. Broader fiscal-year representation remains separate work under +[#1807](https://github.com/PolicyEngine/policyengine-uk/issues/1807). + +## Remaining limits + +HMRC describes a [standard annual method and an alternative method][directors]. +The alternative method uses pay-period calculations during the year and +reconciles to the annual amount at year end. This page and the model's new +route concern the supported full-year annual liability, not either method's +in-year payment sequence. + +Appointment years, category changes, multiple employments, other years and +employer-side director NI remain outside this implementation. The separate +employer rate and threshold in HMRC's table are not employee inputs. An +explicit contribution output supplied by a user retains the existing input +override contract; the new fact does not validate that supplied amount. + +This is a partial response to +[#721](https://github.com/PolicyEngine/policyengine-uk/issues/721), which +remains open. It introduces no director imputation, population estimate or +encoded policy-parity claim. ## References -- HMRC, [NIM12000 — Class 1 NICs: company directors](https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim12000) — primary internal guidance covering the annual cumulative basis. -- HMRC, [CA44 — National Insurance for company directors](https://www.gov.uk/government/publications/ca44-national-insurance-for-company-directors) — the operational employer guide. -- [The Social Security (Contributions) Regulations 2001 (SI 2001/1004) reg. 8](https://www.legislation.gov.uk/uksi/2001/1004/regulation/8) — the regulation that imposes the annual earnings period. -- [Social Security Contributions and Benefits Act 1992 s. 8](https://www.legislation.gov.uk/ukpga/1992/4/section/8) — primary statute for Class 1 employee NI. +- [Social Security (Contributions) Regulations 2001, regulation 8][reg8] — earnings periods for directors. +- [Social Security Contributions and Benefits Act 1992, section 8](https://www.legislation.gov.uk/ukpga/1992/4/section/8) — calculation of primary Class 1 contributions. +- [HMRC NIM12000 — Class 1 NICs: company directors](https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim12000). +- [HMRC CA44 — National Insurance for company directors](https://www.gov.uk/government/publications/ca44-national-insurance-for-company-directors). -[nim12000]: https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim12000 +[rates]: https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023 +[directors]: https://www.gov.uk/employee-directors +[reg8]: https://www.legislation.gov.uk/uksi/2001/1004/regulation/8 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/employee_scale.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/employee_scale.yaml new file mode 100644 index 0000000000..81fb7cd75f --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/employee_scale.yaml @@ -0,0 +1,23 @@ +description: The annual primary Class 1 National Insurance schedule for a full-year category A director in one employment in 2022–23. +brackets: + - threshold: + 2022-04-06: 0 + rate: + 2022-04-06: 0 + - threshold: + 2022-04-06: 11908 + rate: + 2022-04-06: 0.1273 + - threshold: + 2022-04-06: 50270 + rate: + 2022-04-06: 0.0273 +metadata: + label: Full-year category A director annual employee NI 2022–23 + threshold_unit: currency-GBP + rate_unit: /1 + reference: + - title: HMRC rates and thresholds for employers 2022 to 2023, annual director primary thresholds and category A rates + href: https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023 + - title: Social Security (Contributions) Regulations 2001, regulation 8(3), annual earnings period + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/8 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/in_effect.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/in_effect.yaml new file mode 100644 index 0000000000..d080a26b80 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23/in_effect.yaml @@ -0,0 +1,11 @@ +description: The model applies the full-year category A director employee schedule for the supported 2022–23 tax year. +values: + 0000-01-01: false + 2022-04-06: true + 2023-04-06: false +metadata: + label: Full-year category A director 2022–23 implementation coverage + unit: bool + reference: + - title: HMRC rates and thresholds for employers 2022 to 2023, company directors + href: https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_full_year_director_2022.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_full_year_director_2022.yaml new file mode 100644 index 0000000000..a000b8c814 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_full_year_director_2022.yaml @@ -0,0 +1,143 @@ +# Literal annual employee-primary expectations from HMRC's 2022–23 director +# table: PT £11,908, UEL £50,270, category A rates 12.73% and 2.73%. +# Annual model amounts, not interim payroll deductions. All are adults below SPA. +- name: Full-year category A director below the annual primary threshold + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 11000 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 0 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 0 + +- name: Full-year category A director at the annual primary threshold + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 11908 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 0 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 0 + +- name: Full-year category A director one pound above the primary threshold + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 11909 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 0.1273 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 0.1273 + +- name: Full-year category A director in the main band + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 30000 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 2303.1116 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 2303.1116 + +- name: Full-year category A director one pound below the upper earnings limit + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 50269 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 4883.3553 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 4883.3553 + +- name: Full-year category A director at the upper earnings limit + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 50270 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 4883.4826 + ni_class_1_employee_additional: 0 + ni_class_1_employee: 4883.4826 + +- name: Full-year category A director one pound above the upper earnings limit + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 50271 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 4883.4826 + ni_class_1_employee_additional: 0.0273 + ni_class_1_employee: 4883.5099 + +- name: Full-year category A director above the upper band + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 100000 + is_full_year_category_a_director: true + output: + ni_class_1_employee_primary: 4883.4826 + ni_class_1_employee_additional: 1357.629 + ni_class_1_employee: 6241.1116 + +- name: Natural salary household pays director NI and unchanged income tax + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + region: LONDON + household_owns_tv: false + employment_income: 30000 + is_full_year_category_a_director: true + would_claim_uc: false + output: + ni_class_1_income: 30000 + ni_liable: true + income_tax: 3486 + ni_class_1_employee: 2303.1116 + national_insurance: 2303.1116 + # Existing calendar-2022 support: council-tax rebate 150 + EBSS 199. + ebr_council_tax_rebate: 150 + ebr_energy_bills_credit: 199 + household_benefits: 349 + tv_licence: 0 + +- name: Natural higher salary household reconciles both director bands and cash + period: 2022 + absolute_error_margin: 0.01 + input: + age: 40 + region: LONDON + household_owns_tv: false + employment_income: 60000 + is_full_year_category_a_director: true + would_claim_uc: false + output: + ni_class_1_income: 60000 + income_tax: 11432 + ni_class_1_employee_primary: 4883.4826 + ni_class_1_employee_additional: 265.629 + ni_class_1_employee: 5149.1116 + national_insurance: 5149.1116 + # Existing calendar-2022 support: council-tax rebate 150 + EBSS 199. + ebr_council_tax_rebate: 150 + ebr_energy_bills_credit: 199 + household_benefits: 349 + tv_licence: 0 diff --git a/policyengine_uk/tests/test_ni_full_year_director_2022.py b/policyengine_uk/tests/test_ni_full_year_director_2022.py new file mode 100644 index 0000000000..d54b2bd78f --- /dev/null +++ b/policyengine_uk/tests/test_ni_full_year_director_2022.py @@ -0,0 +1,273 @@ +"""The declared 2022–23 annual employee schedule, not interim director payroll.""" + +from pathlib import Path + +import numpy as np +import pytest +from policyengine_core.parameters import load_parameter_file + +import policyengine_uk +from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.utils.scenario import Scenario + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +DIRECTORS = "gov.hmrc.national_insurance.class_1.directors.full_year_category_a_2022_23" +DIRECTORY = ( + Path(policyengine_uk.__file__).parent + / "parameters/gov/hmrc/national_insurance/class_1/directors/full_year_category_a_2022_23" +) +COMPONENTS = ( + "ni_class_1_employee_primary", + "ni_class_1_employee_additional", + "ni_class_1_employee", +) + + +def situation(year=2022, flags=(True,), earnings=(30000,), age=40): + people = {} + for i, earnings_i in enumerate(earnings): + people[f"p{i}"] = { + "age": {year: age}, + "employment_income": {year: earnings_i}, + } + if flags[i] is not None: + people[f"p{i}"]["is_full_year_category_a_director"] = {year: flags[i]} + return { + "people": people, + "benunits": {f"b{i}": {"members": [p]} for i, p in enumerate(people)}, + "households": {"h": {"members": list(people)}}, + } + + +def test_raw_coverage_and_scale_metadata(): + coverage = load_parameter_file(str(DIRECTORY / "in_effect.yaml"), "in_effect") + for date, expected in [ + ("2022-04-05", False), + ("2022-04-06", True), + ("2023-04-05", True), + ("2023-04-06", False), + ]: + assert coverage(date) == expected + scale = load_parameter_file( + str(DIRECTORY / "employee_scale.yaml"), "employee_scale" + ) + assert scale.metadata["threshold_unit"] == "currency-GBP" + assert scale.metadata["rate_unit"] == "/1" + assert scale.metadata["reference"] + assert not scale.metadata.get("uprating") + for bracket, threshold, rate in zip( + scale.brackets, [0, 11908, 50270], [0, 0.1273, 0.0273] + ): + assert bracket.threshold("2022-04-06") == threshold + assert bracket.rate("2022-04-06") == rate + for leaf in (bracket.threshold, bracket.rate): + assert [v.instant_str for v in leaf.values_list] == ["2022-04-06"] + assert not leaf.metadata.get("uprating") + + +def test_processed_gate_and_input_metadata(): + system = CountryTaxBenefitSystem() + for year, active in [(2021, False), (2022, True), (2023, False), (2030, False)]: + node = system.parameters( + str(year) + ).gov.hmrc.national_insurance.class_1.directors + assert node.full_year_category_a_2022_23.in_effect == active + scale = system.parameters("2022").gov.hmrc.national_insurance.class_1.directors + assert scale.full_year_category_a_2022_23.employee_scale.thresholds == [ + 0, + 11908, + 50270, + ] + assert scale.full_year_category_a_2022_23.employee_scale.rates == [ + 0, + 0.1273, + 0.0273, + ] + variable = system.variables["is_full_year_category_a_director"] + assert variable.default_value is False + assert variable.value_type is bool + assert "2022–23 only" in variable.label + + +@pytest.mark.parametrize("year", [2021, 2022, 2023, 2026]) +def test_default_false_and_out_of_year_true_preserve_ordinary(year): + baseline = Simulation(situation=situation(year, (None,))) + false = Simulation(situation=situation(year, (False,))) + true = Simulation(situation=situation(year, (True,))) + for variable in (*COMPONENTS, "ni_class_1_employer"): + expected = baseline.calculate(variable, year) + np.testing.assert_array_equal(false.calculate(variable, year), expected) + if year != 2022 or variable == "ni_class_1_employer": + np.testing.assert_array_equal(true.calculate(variable, year), expected) + + +def test_monthly_allocation_components_person_mask_and_query_order(): + data = situation(flags=(True, True, False), earnings=(30000, 60000, 30000)) + first = Simulation(situation=data) + second = Simulation(situation=data) + expected = [2303.1116, 5149.1116, 2665.9] + # Query the final total first, then reverse the component query order. + np.testing.assert_allclose( + first.calculate(COMPONENTS[2], 2022), expected, atol=0.01 + ) + additional = second.calculate(COMPONENTS[1], 2022) + primary = second.calculate(COMPONENTS[0], 2022) + np.testing.assert_allclose(primary + additional, expected, atol=0.01) + np.testing.assert_allclose( + second.calculate(COMPONENTS[2], 2022), expected, atol=0.01 + ) + for variable in COMPONENTS: + months = [ + first.calculate(variable, f"2022-{month:02d}") for month in range(1, 13) + ] + np.testing.assert_allclose( + sum(months), first.calculate(variable, 2022), atol=0.01 + ) + # Equal allocations follow annual inputs; they are not observed payroll cash. + for month in months[1:]: + np.testing.assert_array_equal(month, months[0]) + + +def test_previous_year_fact_does_not_extend_implementation_coverage(): + data = situation() + data["people"]["p0"]["age"][2023] = 41 + data["people"]["p0"]["employment_income"][2023] = 30000 + sim = Simulation(situation=data) + baseline = Simulation(situation=situation(2023, (None,))) + np.testing.assert_array_equal( + sim.calculate("ni_class_1_employee", 2023), + baseline.calculate("ni_class_1_employee", 2023), + ) + + +@pytest.mark.parametrize("age", [15, 80]) +def test_existing_final_age_gate_remains_authoritative(age): + # Inconsistent declared status deliberately tests composition, not entitlement. + sim = Simulation(situation=situation(age=age)) + assert sim.calculate("ni_class_1_employee", 2022)[0] == 0 + + +def test_ni_earnings_composition_is_not_replaced_with_salary(): + data = situation(earnings=(28000,)) + data["people"]["p0"].update( + statutory_sick_pay={2022: 2000}, + employee_pension_contributions_reported={2022: 1000}, + ) + sim = Simulation(situation=data) + assert sim.calculate("ni_class_1_income", 2022)[0] == 30000 + assert sim.calculate("ni_class_1_employee", 2022)[0] == pytest.approx( + 2303.1116, abs=0.01 + ) + + +def test_explicit_final_input_remains_authoritative(): + data = situation() + data["people"]["p0"]["ni_class_1_employee"] = {2022: 123} + sim = Simulation(situation=data) + assert sim.calculate("ni_class_1_employee", 2022)[0] == pytest.approx(123, abs=0.01) + + +def test_director_scale_reform_changes_only_the_declared_route(): + data = situation(flags=(True, False), earnings=(30000, 30000)) + + def reform_schedule(simulation): + system = simulation.tax_benefit_system + system.reset_parameters() + scale = system.parameters.gov.hmrc.national_insurance.class_1.directors + scale.full_year_category_a_2022_23.employee_scale.brackets[1].rate.update( + period="year:2022-01-01:1", value=0.2 + ) + system.process_parameters() + + sim = Simulation( + situation=data, + scenario=Scenario( + simulation_modifier=reform_schedule, applied_before_data_load=True + ), + ) + np.testing.assert_allclose( + sim.calculate("ni_class_1_employee", 2022), [3618.4, 2665.9], atol=0.01 + ) + + +def test_natural_cash_support_is_unchanged_by_director_status(): + # This is the existing model's calendar-2022 support, not a new rate rule. + values = [] + other_taxes = [] + for flag, ni in [(False, 2665.9), (True, 2303.1116)]: + data = situation(flags=(flag,)) + data["benunits"]["b0"]["would_claim_uc"] = {2022: False} + data["households"]["h"]["region"] = {2022: "LONDON"} + data["households"]["h"]["household_owns_tv"] = {2022: False} + sim = Simulation(situation=data) + assert sim.calculate("ebr_council_tax_rebate", 2022)[0] == 150 + assert sim.calculate("ebr_energy_bills_credit", 2022)[0] == 199 + assert sim.calculate("household_benefits", 2022)[0] == 349 + assert sim.calculate("tv_licence", 2022)[0] == 0 + assert sim.calculate("income_tax", 2022)[0] == 3486 + assert sim.calculate("national_insurance", 2022)[0] == pytest.approx( + ni, abs=0.01 + ) + actual_cash = sim.calculate("household_net_income", 2022)[0] + other_taxes.append(float(sim.calculate("household_tax", 2022)[0]) - ni) + values.append(float(actual_cash)) + # Only employee NI changes: the literal cash gain equals the NI reduction. + # Tolerance reflects float32 household totals, without a payroll rounding rule. + assert values[1] - values[0] == pytest.approx(362.7884, abs=0.01) + assert other_taxes[0] == pytest.approx(other_taxes[1], abs=0.01) + + +def test_director_primary_propagates_through_existing_class_4_cap(): + """Composition of existing rules, not a new standalone Class 4 law oracle.""" + results = [] + # Profit is net of £2,000 expenses: gross receipts exceed profit by more + # than the trading allowance. No loss, capital allowance or output is supplied. + for flag, primary, cap, total_ni in [ + (False, 2665.9, 2082.6041179856, 4912.3041179856), + (True, 2303.1116, 2343.6029669065, 4810.5145669065), + ]: + data = situation(flags=(flag,)) + data["people"]["p0"].update( + self_employment_income={2022: 60000}, + self_employment_gross_receipts={2022: 62000}, + ) + data["benunits"]["b0"]["would_claim_uc"] = {2022: False} + data["households"]["h"].update( + region={2022: "LONDON"}, household_owns_tv={2022: False} + ) + sim = Simulation(situation=data) + assert sim.calculate("trading_allowance_deduction", 2022)[0] == 0 + assert sim.calculate("ni_class_4_profits", 2022)[0] == 60000 + assert sim.calculate("ni_class_1_employee_primary", 2022)[0] == pytest.approx( + primary, abs=0.01 + ) + assert sim.calculate("ni_class_1_employee_additional", 2022)[0] == 0 + assert sim.calculate("ni_class_2", 2022)[0] == pytest.approx(163.8, abs=0.01) + # Existing 2022 parameters: UPL50270, LPL11908, Class4rates.0973/.0273, + # Class2weekly3.15. AboveUPL, case1false. Headroom =38362*.0973 + # +53*3.15 -52*3.15 -primary. Remaining main band is charged at.0273. + assert sim.calculate("ni_class_4_maximum", 2022)[0] == pytest.approx( + cap, abs=0.01 + ) + assert cap < 3998.2516 # Uncapped Class 4:38362*.0973+9730*.0273. + assert sim.calculate("ni_class_4", 2022)[0] == pytest.approx(cap, abs=0.01) + assert sim.calculate("national_insurance", 2022)[0] == pytest.approx( + total_ni, abs=0.01 + ) + results.append( + { + variable: float(sim.calculate(variable, 2022)[0]) + for variable in [ + "household_net_income", + "household_benefits", + "income_tax", + "ni_class_1_employer", + ] + } + ) + for variable in ["household_benefits", "income_tax", "ni_class_1_employer"]: + assert results[0][variable] == pytest.approx(results[1][variable], abs=0.01) + # Class 1 falls362.7884, Class4rises260.9988489; final cash gain is101.7895511. + assert results[1]["household_net_income"] - results[0][ + "household_net_income" + ] == pytest.approx(101.7895511, abs=0.01) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py index 6f8dffd2c2..ef7b99db7f 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py @@ -16,4 +16,19 @@ def formula(person, period, parameters): parameters.thresholds.upper_earnings_limit * WEEKS_IN_YEAR / MONTHS_IN_YEAR ) upper_earnings_limit_income = max_(income - upper_earnings_limit, 0) - return parameters.rates.employee.additional * upper_earnings_limit_income + ordinary = parameters.rates.employee.additional * upper_earnings_limit_income + directors = parameters.directors.full_year_category_a_2022_23 + if not directors.in_effect: + return ordinary + + # Match the annual primary component; this is not an interim payroll charge. + scale = directors.employee_scale + annual_income = person("ni_class_1_income", period.this_year) + annual_additional = scale.rates[2] * max_( + annual_income - scale.thresholds[2], 0 + ) + return where( + person("is_full_year_category_a_director", period.this_year), + annual_additional / MONTHS_IN_YEAR, + ordinary, + ) diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py index 69b5db4986..f29602fa40 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py @@ -26,4 +26,20 @@ def formula(person, period, parameters): max_(income - primary_threshold, 0) - upper_earnings_limit_income ) - return parameters.rates.employee.main * primary_threshold_income + ordinary = parameters.rates.employee.main * primary_threshold_income + directors = parameters.directors.full_year_category_a_2022_23 + if not directors.in_effect: + return ordinary + + # These are annual earnings-period amounts, allocated evenly to model months. + scale = directors.employee_scale + annual_income = person("ni_class_1_income", period.this_year) + annual_primary = scale.rates[1] * min_( + max_(annual_income - scale.thresholds[1], 0), + scale.thresholds[2] - scale.thresholds[1], + ) + return where( + person("is_full_year_category_a_director", period.this_year), + annual_primary / MONTHS_IN_YEAR, + ordinary, + ) diff --git a/policyengine_uk/variables/input/is_full_year_category_a_director.py b/policyengine_uk/variables/input/is_full_year_category_a_director.py new file mode 100644 index 0000000000..55e57a5b0a --- /dev/null +++ b/policyengine_uk/variables/input/is_full_year_category_a_director.py @@ -0,0 +1,18 @@ +from policyengine_uk.model_api import * + + +class is_full_year_category_a_director(Variable): + label = "Full-year category A director, one employment (employee NI, 2022–23 only)" + documentation = ( + "For employee NI only, explicitly declares directorship throughout the tax year, one employment " + "and category A throughout. Only model year 2022 uses this fact. It does " + "not describe appointment years, other categories or payroll timing." + ) + entity = Person + definition_period = YEAR + value_type = bool + default_value = False + reference = [ + "https://www.gov.uk/employee-directors", + "https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023", + ]