diff --git a/changelog.d/222-ni-scale-parameters.changed.md b/changelog.d/222-ni-scale-parameters.changed.md new file mode 100644 index 0000000000..afed05cb16 --- /dev/null +++ b/changelog.d/222-ni-scale-parameters.changed.md @@ -0,0 +1,4 @@ +- Store marginal National Insurance schedules as parameter scales while preserving contribution calculations and existing scalar reform paths. +- Reject unsupported changes to the fixed zero-rate, zero-threshold NI scale sentinel with a targeted error. +- Direct YAML readers must migrate the ten removed scalar files to the canonical scale files; parameter-tree readers retain the legacy compatibility paths. +- Retain incoming savings indexation, director and liability rules while migrating NI readers; hide legacy and fixed-zero discovery controls without removing their IDs or update contracts. diff --git a/docs/book/index.md b/docs/book/index.md index b4949f3ee0..77e48adc15 100644 --- a/docs/book/index.md +++ b/docs/book/index.md @@ -67,3 +67,7 @@ revenue = ( ) f"Revenue: £{round(revenue / 1e9, 1)}bn" ``` + +## Model reference + +- [National Insurance parameter scales](programs/gov/hmrc/national-insurance-scales.md) diff --git a/docs/book/policy/model-baseline.md b/docs/book/policy/model-baseline.md index 138f52dd4b..216a78ee1c 100644 --- a/docs/book/policy/model-baseline.md +++ b/docs/book/policy/model-baseline.md @@ -8,7 +8,7 @@ PolicyEngine UK models the UK tax and benefit system as of 2025, incorporating m ### Budget 2025 threshold freezes -Budget 2025 keeps the income tax personal allowance and basic rate limit, and the equivalent National Insurance thresholds, at their current levels until April 2031. Finance Act 2021 s.5, as amended by Finance Act 2023 s.5 and Finance Act 2026 s.10, fixes the [personal allowance](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml) at £12,570 and the [basic rate limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml) at £37,700 for 2022-23 to 2030-31. The model holds the [primary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml) and [lower profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml) at £12,570, the [upper earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml) and [upper profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml) at £50,270, and the [secondary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml) at £5,000 a year through 2030-31. For Class 1, regulations set only the 2026-27 amounts; the later years are announced policy. The Class 4 limits are standing figures in SSCBA 1992 s.15(3). The [lower earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/lower_earnings_limit.yaml) is not frozen: it rose with CPI to £129 a week in 2026-27. +Budget 2025 keeps the income tax personal allowance and basic rate limit, and the equivalent National Insurance thresholds, at their current levels until April 2031. Finance Act 2021 s.5, as amended by Finance Act 2023 s.5 and Finance Act 2026 s.10, fixes the [personal allowance](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/allowances/personal_allowance/amount.yaml) at £12,570 and the [basic rate limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/income_tax/rates/uk.yaml) at £37,700 for 2022-23 to 2030-31. The model holds the [primary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml) and [lower profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml) at £12,570, the [upper earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml) and [upper profits limit](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml) at £50,270, and the [secondary threshold](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml) at £5,000 a year through 2030-31. For Class 1, regulations set only the 2026-27 amounts; the later years are announced policy. The Class 4 limits are standing figures in SSCBA 1992 s.15(3). The [lower earnings limit](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/lower_earnings_limit.yaml) is not frozen: it rose with CPI to £129 a week in 2026-27. From 2031-32 the model indexes these thresholds by September CPI, in [`create_threshold_indexation.py`](https://github.com/PolicyEngine/policyengine-uk/blob/main/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The personal allowance and basic rate limit follow Income Tax Act 2007 ss.57 and 21 year by year: each rises by the CPI increase to the previous September, with the allowance's increase rounded up to a multiple of £10 and the limit rounded up to a multiple of £100. The government says the National Insurance thresholds will also be uprated by September CPI after 2030-31 (Explanatory Memorandum to SI 2026/231, para 5.1), and it has kept them aligned with income tax through the freeze (HMRC's Budget 2025 policy paper on the threshold freeze; Explanatory Memorandum to SI 2026/231, paras 5.14 and 5.15). The model assumes the alignment continues, round-ups included: the primary threshold and lower profits limit equal the personal allowance, and the upper earnings and profits limits equal the higher rate threshold (the personal allowance plus the basic rate limit). It also assumes the secondary threshold rises by September CPI, unrounded; the Autumn Budget 2024 policy paper says only that it will be increased in line with CPI. None of these National Insurance rules after 2030-31 is law. @@ -16,7 +16,7 @@ From 2031-32 the model indexes these thresholds by September CPI, in [`create_th ### Autumn Budget 2024 -The government increased the [employer National Insurance rate from 13.8% to 15%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml#L30-L35) and reduced the [employer secondary threshold from £9,100 to £5,000 annually](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml#L26-L30), both taking effect in fiscal year 2025-26. +The government increased the [employer National Insurance rate from 13.8% to 15%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml) and reduced the [employer secondary threshold from £9,100 to £5,000 annually](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml), both taking effect in fiscal year 2025-26. ### Universal Credit rebalancing @@ -30,7 +30,7 @@ Annual benefit uprating applied 4.1% increases to state pensions and 1.7% to wor ### Autumn Statement 2023 -The government announced further National Insurance cuts, reducing the [employee main rate from 10% to 8%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml#L24-L29) and the [self-employed Class 4 rate from 9% to 6%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml#L26-L33) in fiscal year 2024-25. [Class 2 National Insurance contributions were abolished](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml#L51-L56) for self-employed people, with the flat rate set to £0 in fiscal year 2024-25. +The government announced further National Insurance cuts, reducing the [employee main rate from 10% to 8%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml) and the [self-employed Class 4 rate from 9% to 6%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml) in fiscal year 2024-25. [Class 2 National Insurance contributions were abolished](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml#L51-L56) for self-employed people, with the flat rate set to £0 in fiscal year 2024-25. ### Capital gains tax changes (Autumn Budget 2024) @@ -42,7 +42,7 @@ The government increased the [child benefit high income tax charge threshold fro ### Autumn Statement 2023 (Employee NI cut) -An initial National Insurance cut reduced the [employee main rate from 12% to 10%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml#L18-L23) in fiscal year 2024-25. +An initial National Insurance cut reduced the [employee main rate from 12% to 10%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml) in fiscal year 2024-25. ## 2023 reforms @@ -54,11 +54,11 @@ The government reduced the [additional rate income tax threshold from £150,000 ### Spring Statement 2022 -The government announced an increase in the [National Insurance primary threshold from £9,880 to £12,570](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml#L17-L22), aligning it with the personal allowance in fiscal year 2022-23. +The government announced an increase in the [National Insurance primary threshold from £9,880 to £12,570](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml), aligning it with the personal allowance in fiscal year 2022-23. ### Autumn Budget 2021 (Health and Social Care Levy) -The Health and Social Care Levy Act 2021 raised the [employee main rate from 12% to 13.25%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml#L4-L17) and the [employer rate from 13.8% to 15.05%](https://github.com/PolicyEngine/policyengine-uk/blob/master/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml#L18-L29) for fiscal year 2022-23. The Health and Social Care Levy (Repeal) Act 2022 restored 12% and 13.8% for earnings paid from 6 November 2022. The model reads each fiscal year's rates at 30 April, so 2022-23 carries the levy rates for the whole year ([#1807](https://github.com/PolicyEngine/policyengine-uk/issues/1807)). +The Health and Social Care Levy Act 2021 raised the [employee main rate from 12% to 13.25%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml) and the [employer rate from 13.8% to 15.05%](https://github.com/PolicyEngine/policyengine-uk/blob/e1601dbe4feb15e71d0d59b7a57f9d860741c068/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml) for fiscal year 2022-23. The Health and Social Care Levy (Repeal) Act 2022 restored 12% and 13.8% for earnings paid from 6 November 2022. The model reads each fiscal year's rates at 30 April, so 2022-23 carries the levy rates for the whole year ([#1807](https://github.com/PolicyEngine/policyengine-uk/issues/1807)). ### Mini-Budget 2022 (September) diff --git a/docs/book/programs/gov/hmrc/national-insurance-scales.md b/docs/book/programs/gov/hmrc/national-insurance-scales.md new file mode 100644 index 0000000000..4d8d8b3769 --- /dev/null +++ b/docs/book/programs/gov/hmrc/national-insurance-scales.md @@ -0,0 +1,93 @@ +# National Insurance parameter scales + +PolicyEngine UK stores three marginal National Insurance (NI) schedules as +parameter scales. Each schedule holds its contribution rates and the thresholds +at which they apply. The schedules retain their dated values and units. + +All paths below start with `gov.hmrc.national_insurance.`. Bracket indices start +at zero. Bracket zero supplies the fixed zero-threshold, zero-rate sentinel. +Contribution reforms use brackets 1 and 2 below. Any nonzero, missing or null +threshold or rate in bracket zero is rejected with a targeted error naming the +canonical leaf. Validation covers its full dated history, including future +entries; an explicit update to zero leaves calculations unchanged. + +| Scale | Contribution bands | Threshold period in the parameter files | +| --- | --- | --- | +| `class_1.employee_scale` | Primary rate in bracket 1; additional rate in bracket 2 | Week | +| `class_1.employer_scale` | Employer rate in bracket 1 | Week | +| `class_4.contribution_scale` | Main rate in bracket 1; additional rate in bracket 2 | Year | + +The contribution formulas retain their period conversions. Changing the +parameter layout does not make a weekly threshold an annual amount. Class 2's +flat contribution amount and its eligibility thresholds remain scalar +parameters. + +## Reform paths + +Use the following canonical paths when specifying new rate or threshold reforms. +The old scalar paths remain live views of the corresponding scale components. +Updating either path changes the same dated parameter history. + +| Existing scalar path | Canonical scale component | +| --- | --- | +| `class_1.rates.employee.main` | `class_1.employee_scale[1].rate` | +| `class_1.rates.employee.additional` | `class_1.employee_scale[2].rate` | +| `class_1.thresholds.primary_threshold` | `class_1.employee_scale[1].threshold` | +| `class_1.thresholds.upper_earnings_limit` | `class_1.employee_scale[2].threshold` | +| `class_1.rates.employer` | `class_1.employer_scale[1].rate` | +| `class_1.thresholds.secondary_threshold` | `class_1.employer_scale[1].threshold` | +| `class_4.rates.main` | `class_4.contribution_scale[1].rate` | +| `class_4.rates.additional` | `class_4.contribution_scale[2].rate` | +| `class_4.thresholds.lower_profits_limit` | `class_4.contribution_scale[1].threshold` | +| `class_4.thresholds.upper_profits_limit` | `class_4.contribution_scale[2].threshold` | + +The bracket syntax above identifies a leaf in a reform path. Python callables +access a raw scale through its `brackets` list, for example +`parameters.gov.hmrc.national_insurance.class_1.employee_scale.brackets[1].rate`. +Existing scalar attribute chains and leaf updates remain supported. The model's +scalar views expose the old names, current dated values, descriptions and units. + +Apply scalar changes through the rate or threshold leaf. The compatibility views +do not provide a second editable copy of the NI schedules. Replacing, deleting or +reparenting legacy subtrees requires an explicit structural migration using the +canonical scales; conflicting legacy replacements raise a migration error. + +## Contribution outputs + +The scales preserve the model's contribution components. Employee Class 1 NI +still adds `ni_class_1_employee_primary` and +`ni_class_1_employee_additional`. These outputs retain their separate input +overrides. Class 4 still calculates its main and additional contributions and +applies the existing annual-maximum rules. Employer NI retains its existing +pension-contribution switch. + +The parameter migration preserves the existing calculation rules, liability +conditions and rounding. + +## Maintaining the schedules + +Edit the canonical scale components when changing the model. Their dated values, +references, units and uprating metadata supply the legacy scalar views. The NI +threshold generator writes to these canonical leaves. Parameter processing visits +each numerical history once; metadata discovery also exposes the legacy names. + +Metadata retains both primitive IDs and their histories and units. Discovery +marks the ten legacy scalar views and the six fixed-zero leaves with +`household: false` and `economy: false`; the ten canonical contribution rate and +threshold leaves remain visible. Direct legacy lookup still resolves the same +canonical metadata and values, while enumeration supplies presentation-only +views. Hiding a fixed-zero control does not change its input-validation rule. + +The pinned, source-extracted API contract tests cover these flags, all other +record fields and update routes. They do not verify a deployed API or the app's +tree, search and control presentation. Consumer owners must check those surfaces +before relying on the migration in a UI. + +The separate full-year Category A director scale and factual-input gate for +2022–23 retain their constrained annual calculation and monthly allocation. +They are not replaced by the ordinary employee schedule. The savings starting +rate remains an annual limit, indexed alongside the five canonical NI threshold +writers without treating it as a weekly amount. + +For NI rate reforms in behavioural scenarios, see +[Labour supply dynamics](../../../usage/dynamics.md). diff --git a/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py b/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py index 275812cdd4..a838ad12dd 100644 --- a/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py +++ b/policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py @@ -134,11 +134,11 @@ "gov.hmrc.income_tax.allowances.personal_allowance.amount", "gov.hmrc.income_tax.rates.uk[1].threshold", "gov.hmrc.income_tax.rates.savings_starter_rate.allowance", - "gov.hmrc.national_insurance.class_1.thresholds.primary_threshold", - "gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit", - "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold", - "gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit", - "gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit", + "gov.hmrc.national_insurance.class_1.employee_scale[1].threshold", + "gov.hmrc.national_insurance.class_1.employee_scale[2].threshold", + "gov.hmrc.national_insurance.class_1.employer_scale[1].threshold", + "gov.hmrc.national_insurance.class_4.contribution_scale[1].threshold", + "gov.hmrc.national_insurance.class_4.contribution_scale[2].threshold", ) @@ -325,8 +325,8 @@ def add_threshold_indexation(parameters: ParameterNode) -> ParameterNode: """ increases = september_cpi_increases(parameters) income_tax = parameters.gov.hmrc.income_tax - class_1 = parameters.gov.hmrc.national_insurance.class_1.thresholds - class_4 = parameters.gov.hmrc.national_insurance.class_4.thresholds + class_1 = parameters.gov.hmrc.national_insurance.class_1 + class_4 = parameters.gov.hmrc.national_insurance.class_4 personal_allowance_parameter = income_tax.allowances.personal_allowance.amount basic_rate_limit_parameter = income_tax.rates.uk.brackets[1].threshold savings_starting_rate_limit_parameter = ( @@ -336,11 +336,11 @@ def add_threshold_indexation(parameters: ParameterNode) -> ParameterNode: personal_allowance_parameter, basic_rate_limit_parameter, savings_starting_rate_limit_parameter, - class_1.primary_threshold, - class_1.upper_earnings_limit, - class_1.secondary_threshold, - class_4.lower_profits_limit, - class_4.upper_profits_limit, + class_1.employee_scale.brackets[1].threshold, + class_1.employee_scale.brackets[2].threshold, + class_1.employer_scale.brackets[1].threshold, + class_4.contribution_scale.brackets[1].threshold, + class_4.contribution_scale.brackets[2].threshold, ] # From the earliest last value in the files, so the income tax amounts # cover every year a NICs threshold takes from them. @@ -365,7 +365,10 @@ def add_threshold_indexation(parameters: ParameterNode) -> ParameterNode: year: personal_allowance[year] + basic_rate_limit[year] for year in years } secondary_threshold = tax_year_path( - class_1.secondary_threshold, years, increases, index_secondary_threshold + class_1.employer_scale.brackets[1].threshold, + years, + increases, + index_secondary_threshold, ) write_unstated_tax_years(personal_allowance_parameter, personal_allowance) @@ -373,20 +376,26 @@ def add_threshold_indexation(parameters: ParameterNode) -> ParameterNode: write_unstated_tax_years( savings_starting_rate_limit_parameter, savings_starting_rate_limit ) - write_unstated_tax_years(class_4.lower_profits_limit, personal_allowance) - write_unstated_tax_years(class_4.upper_profits_limit, higher_rate_threshold) write_unstated_tax_years( - class_1.primary_threshold, + class_4.contribution_scale.brackets[1].threshold, personal_allowance + ) + write_unstated_tax_years( + class_4.contribution_scale.brackets[2].threshold, higher_rate_threshold + ) + write_unstated_tax_years( + class_1.employee_scale.brackets[1].threshold, {year: weekly_equivalent(value) for year, value in personal_allowance.items()}, ) write_unstated_tax_years( - class_1.upper_earnings_limit, + class_1.employee_scale.brackets[2].threshold, { year: weekly_equivalent(value) for year, value in higher_rate_threshold.items() }, ) - write_unstated_tax_years(class_1.secondary_threshold, secondary_threshold) + write_unstated_tax_years( + class_1.employer_scale.brackets[1].threshold, secondary_threshold + ) for parameter in indexed: (parameter.metadata or {}).pop(FILES_LAST_TAX_YEAR, None) return parameters diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employee_scale.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employee_scale.yaml new file mode 100644 index 0000000000..fa604f14b4 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employee_scale.yaml @@ -0,0 +1,244 @@ +description: Class 1 employee marginal contribution schedule. +metadata: + label: Class 1 employee marginal contribution schedule + threshold_unit: currency-GBP + rate_unit: /1 +brackets: + - threshold: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: currency-GBP + period: week + rate: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: /1 + - threshold: + description: The Primary Threshold is the lower bound for the main rate of Class 1 National Insurance. + # From 6 July 2022 the model stores the annual earnings period threshold + # divided by 52 (12,570 / 52, rounded to 241.73), as SI 2001/1004 reg 11(3)(c) + # does for periods of whole weeks; 52 weeks give £12,569.96. The weekly amount + # in reg 10(c) is £242. + # Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The + # 2027-28 to 2030-31 values are announced policy (Autumn Statement 2022 and + # Budget 2025), to be set in each year's rates, limits and thresholds + # regulations. + # From 2031-32 (the tax year after the last value below) the model keeps the + # threshold at the personal allowance over 52, to the penny, as for 241.73 + # (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The + # government has kept the PT aligned with the personal allowance, which Income + # Tax Act 2007 s57 indexes by September CPI from 2031-32, and says the PT will + # be uprated by September CPI after 2030-31 (Explanatory Memorandum to SI + # 2026/231 para 5.1). That is announced policy, not law; keeping it equal to + # the allowance, round-up included, is a modelling assumption consistent with + # it. The file has no uprating. + values: + 2015-04-06: 155 + 2016-04-06: 155 + 2017-04-06: 157 + 2018-04-06: 162 + 2019-04-06: 166 + 2020-04-06: 183 + 2021-04-06: 184 + 2022-04-06: + value: 190 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2022(6) + href: https://www.legislation.gov.uk/uksi/2022/232/regulation/6/made#regulation-6-2-a + 2022-07-01: + value: 241.73 + metadata: + reference: + - title: Spring Statement 2022 + href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1062486/Spring_Statement_2022_Web_Accessible.pdf + 2026-04-06: + value: 241.73 + metadata: + reference: + - title: The Social Security (Contributions) Regulations 2001 reg 10(c) and reg 11(3), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 + - title: SI 2026/231 reg 5 + href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made + 2027-04-06: + value: 241.73 + metadata: + reference: + - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" + href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 + 2028-04-06: + value: 241.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2029-04-06: + value: 241.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2030-04-06: + value: 241.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + metadata: + label: NI primary threshold + unit: currency-GBP + period: week + reference: + - title: "GOV.UK | Rates and allowances: National Insurance contributions" + href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + rate: + description: The Class 1 National Insurance main rate is paid on employment earnings between the Primary Threshold and the Upper Earnings Limit. + values: + 2015-04-01: 0.12 + 2022-04-06: + value: 0.1325 + metadata: + reference: + - title: Health and Social Care Levy Act 2021 s.5(2)(a)(i) + href: https://www.legislation.gov.uk/ukpga/2021/28/section/5/enacted + - title: Autumn Budget and Spending Review 2022 + href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf + 2022-11-06: + value: 0.12 + metadata: + reference: + - title: Health and Social Care Levy Act (Repeal) 2022 + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + 2024-01-01: + value: 0.1 + metadata: + reference: + - title: "Autumn Statement 2023: National Insurance Factsheet" + href: https://www.gov.uk/government/publications/autumn-statement-2023-national-insurance-factsheet/autumn-statement-2023-national-insurance-factsheet + 2024-04-01: + value: 0.08 + metadata: + reference: + - title: National Insurance Contributions (Reduction in Rates) Act 2024 s 1 + href: https://www.legislation.gov.uk/ukpga/2024/5/section/1 + metadata: + label: NI class 1 main rate + unit: /1 + reference: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + - threshold: + description: The Upper Earnings Limit is the upper bound for the main rate of Class 1 National Insurance. + # From 2021-22 the model stores the annual earnings period limit divided by 52 + # (50,270 / 52, rounded to 966.73), as SI 2001/1004 reg 11(2A)(c) does for + # periods of whole weeks; 52 weeks give £50,269.96. The weekly amount in + # reg 10(b) is £967. + # Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The + # 2027-28 to 2030-31 values are announced policy (Autumn Statement 2022 and + # Budget 2025), to be set in each year's rates, limits and thresholds + # regulations. + # From 2031-32 (the tax year after the last value below) the model keeps the + # limit at the higher rate threshold (personal allowance plus basic rate + # limit) over 52, to the penny, as for 966.73 + # (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The + # UEL has been aligned with the higher rate threshold since April 2009, and + # for 2026-27 "will remain at a level equivalent to the annual HRT" + # (Explanatory Memorandum to SI 2026/231 paras 5.14-5.15); para 5.1 says it + # will be uprated by September CPI after 2030-31. Income Tax Act 2007 ss21 and + # 57 index the higher rate threshold by September CPI from 2031-32. Keeping + # the UEL equal to it, round-ups included, is a modelling assumption + # consistent with that announced policy, not law. The file has no uprating. + values: + 2015-04-06: 815 + 2016-04-06: 827 + 2017-04-06: 866 + 2018-04-06: 892 + 2019-04-06: 962 + 2020-04-06: 962 + 2021-04-06: 966.73 + 2026-04-06: + value: 966.73 + metadata: + reference: + - title: The Social Security (Contributions) Regulations 2001 reg 10(b) and reg 11(2A), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 + - title: SI 2026/231 reg 5 + href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made + 2027-04-06: + value: 966.73 + metadata: + reference: + - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" + href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 + 2028-04-06: + value: 966.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2029-04-06: + value: 966.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2030-04-06: + value: 966.73 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + metadata: + label: NI upper earnings limit + unit: currency-GBP + period: week + reference: + - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + rate: + description: The Class 1 National Insurance additional rate is paid on employment earnings above the Upper Earnings Limit. + values: + 2015-04-01: 0.02 + 2022-04-01: + value: 0.0325 + metadata: + reference: + - title: Autumn Budget and Spending Review 2022 + href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf + 2024-04-04: + value: 0.02 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/8 + metadata: + label: NI class 1 additional rate + unit: /1 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employer_scale.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employer_scale.yaml new file mode 100644 index 0000000000..03a0362bed --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/employer_scale.yaml @@ -0,0 +1,161 @@ +description: Class 1 employer marginal contribution schedule. +metadata: + label: Class 1 employer marginal contribution schedule + threshold_unit: currency-GBP + rate_unit: /1 +brackets: + - threshold: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: currency-GBP + period: week + rate: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: /1 + - threshold: + description: Secondary threshold for National Insurance. + metadata: + label: NI secondary threshold + period: week + reference: + - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + unit: currency-GBP + # From 2025-26 the value is the annual £5,000 in SI 2001/1004 reg 11(3A)(b) + # divided by 52 (96.153846 = 5,000 / 52), so that the model's 52-week + # annualisation gives £5,000. This is the convention reg 11(3)(c) and + # 11(2A)(c) use for the primary threshold and upper earnings limit, which the + # model also stores as annual / 52. The rounded weekly figure in reg 10(d) is + # £96, which would annualise to £4,992 (#1967). Before 2025-26 the weekly + # figure times 52 equals the annual one (for example £175 x 52 = £9,100). + # Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The + # 2027-28 to 2030-31 values are announced policy (Autumn Budget 2024 and + # Budget 2025), to be set in each year's rates, limits and thresholds + # regulations. + # From 2031-32 (the tax year after the last value below) the model raises the + # threshold each year by September CPI of the previous year, unrounded + # (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The + # Autumn Budget 2024 note said the threshold "will be increased in line with + # Consumer Prices Index (CPI)" after its freeze; nothing later sets the rule, + # so this is a modelling assumption, and the file has no uprating. + values: + # £155 was the primary threshold in 2015-16 and 2016-17 (reg 10(c)), not the + # secondary threshold (#2178). + 2015-04-06: + value: 156 + metadata: + reference: + - title: The Social Security (Contributions) Regulations 2001 reg 10(d) (£156 a week), as at 6 April 2015 + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/10/2015-04-06 + - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£8,112 where the earnings period is a year), as at 6 April 2015 + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2015-04-06 + 2016-04-06: + value: 156 + metadata: + reference: + - title: The Social Security (Contributions) Regulations 2001 reg 10(d) (£156 a week), as at 6 April 2016 + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/10/2016-04-06 + - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£8,112 where the earnings period is a year), as at 6 April 2016 + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2016-04-06 + 2017-04-06: 157 + 2018-04-06: 162 + 2019-04-06: 166 + 2020-04-06: 169 + 2021-04-06: 170 + 2022-04-06: + metadata: + reference: + - href: https://www.legislation.gov.uk/uksi/2022/232/regulation/6/made#regulation-6-2-a + title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments + and National Insurance Funds Payments) Regulations 2022(6) + value: 175 + 2024-01-01: 175 + 2025-04-06: + value: 96.153846 + metadata: + reference: + - title: National Insurance Contributions (Secondary Class 1 Contributions) Act 2025 s.2 + href: https://www.legislation.gov.uk/ukpga/2025/11/section/2 + - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£5,000 where the earnings period is a year) + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11 + 2026-04-06: + value: 96.153846 + metadata: + reference: + - title: The Social Security (Contributions) Regulations 2001 reg 10(d) and reg 11(3A), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) + href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 + - title: SI 2026/231 reg 5 + href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made + 2027-04-06: + value: 96.153846 + metadata: + reference: + - title: "HMRC: Changes to the Class 1 National Insurance Contributions Secondary Threshold, the Secondary Class 1 National Insurance contributions rate, and the Employment Allowance from 6 April 2025 (Autumn Budget 2024)" + href: https://www.gov.uk/government/publications/changes-to-the-class-1-national-insurance-contributions-secondary-threshold-the-secondary-class-1-national-insurance-contributions-rate-and-the-empl/changes-to-the-class-1-national-insurance-contributions-secondary-threshold-the-secondary-class-1-national-insurance-contributions-rate-and-the-empl + 2028-04-06: + value: 96.153846 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.112 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: Explanatory Memorandum to SI 2026/231 para 5.26 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2029-04-06: + value: 96.153846 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.112 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: Explanatory Memorandum to SI 2026/231 para 5.26 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2030-04-06: + value: 96.153846 + metadata: + reference: + - title: Budget 2025 (HC 1492) para 4.112 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: Explanatory Memorandum to SI 2026/231 para 5.26 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + rate: + description: National Insurance contribution rate by employers on earnings above the + Secondary Threshold. + metadata: + label: NI employer rate + propagate_metadata_to_children: true + unit: /1 + # Annual values are the rate at 30 April of each tax year, as for the + # employee main rate (no fiscal_year_blend). For 2022-23 that is the levy + # rate of 15.05%, although earnings paid from 6 November 2022 were charged + # at 13.8%; see #1807 for the choice between sampling and averaging. + values: + 2015-04-01: + value: 0.138 + metadata: + reference: + - title: National Insurance rates and thresholds + href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + 2022-04-06: + value: 0.1505 + metadata: + reference: + - title: Health and Social Care Levy Act 2021 s.5(2)(b) + href: https://www.legislation.gov.uk/ukpga/2021/28/section/5/enacted + 2022-11-06: + value: 0.138 + metadata: + reference: + - title: Health and Social Care Levy (Repeal) Act 2022 s.2(1) + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2/enacted + 2025-01-01: + value: 0.15 + metadata: + reference: + - title: National Insurance Contributions (Secondary Class 1 Contributions) Act 2025 s.1 + href: https://www.legislation.gov.uk/ukpga/2025/11/section/1 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml deleted file mode 100644 index 717968b8a2..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/additional.yaml +++ /dev/null @@ -1,18 +0,0 @@ -description: The Class 1 National Insurance additional rate is paid on employment earnings above the Upper Earnings Limit. -values: - 2015-04-01: 0.02 - 2022-04-01: - value: 0.0325 - metadata: - reference: - - title: Autumn Budget and Spending Review 2022 - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf - 2024-04-04: - value: 0.02 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 - href: https://www.legislation.gov.uk/ukpga/1992/4/section/8 -metadata: - label: NI class 1 additional rate - unit: /1 \ No newline at end of file diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml deleted file mode 100644 index f7e10bfa97..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employee/main.yaml +++ /dev/null @@ -1,33 +0,0 @@ -description: The Class 1 National Insurance main rate is paid on employment earnings between the Primary Threshold and the Upper Earnings Limit. -values: - 2015-04-01: 0.12 - 2022-04-06: - value: 0.1325 - metadata: - reference: - - title: Health and Social Care Levy Act 2021 s.5(2)(a)(i) - href: https://www.legislation.gov.uk/ukpga/2021/28/section/5/enacted - - title: Autumn Budget and Spending Review 2022 - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf - 2022-11-06: - value: 0.12 - metadata: - reference: - - title: Health and Social Care Levy Act (Repeal) 2022 - href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 - 2024-01-01: - value: 0.1 - metadata: - reference: - - title: "Autumn Statement 2023: National Insurance Factsheet" - href: https://www.gov.uk/government/publications/autumn-statement-2023-national-insurance-factsheet/autumn-statement-2023-national-insurance-factsheet - 2024-04-01: - value: 0.08 - metadata: - reference: - - title: National Insurance Contributions (Reduction in Rates) Act 2024 s 1 - href: https://www.legislation.gov.uk/ukpga/2024/5/section/1 -metadata: - label: NI class 1 main rate - unit: /1 - reference: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml deleted file mode 100644 index 55ca38508c..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/rates/employer.yaml +++ /dev/null @@ -1,35 +0,0 @@ -description: National Insurance contribution rate by employers on earnings above the - Secondary Threshold. -metadata: - label: NI employer rate - propagate_metadata_to_children: true - unit: /1 - # Annual values are the rate at 30 April of each tax year, as for the - # employee main rate (no fiscal_year_blend). For 2022-23 that is the levy - # rate of 15.05%, although earnings paid from 6 November 2022 were charged - # at 13.8%; see #1807 for the choice between sampling and averaging. -values: - 2015-04-01: - value: 0.138 - metadata: - reference: - - title: National Insurance rates and thresholds - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - 2022-04-06: - value: 0.1505 - metadata: - reference: - - title: Health and Social Care Levy Act 2021 s.5(2)(b) - href: https://www.legislation.gov.uk/ukpga/2021/28/section/5/enacted - 2022-11-06: - value: 0.138 - metadata: - reference: - - title: Health and Social Care Levy (Repeal) Act 2022 s.2(1) - href: https://www.legislation.gov.uk/ukpga/2022/43/section/2/enacted - 2025-01-01: - value: 0.15 - metadata: - reference: - - title: National Insurance Contributions (Secondary Class 1 Contributions) Act 2025 s.1 - href: https://www.legislation.gov.uk/ukpga/2025/11/section/1 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml deleted file mode 100644 index 2f934569d5..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/primary_threshold.yaml +++ /dev/null @@ -1,89 +0,0 @@ -description: The Primary Threshold is the lower bound for the main rate of Class 1 National Insurance. -# From 6 July 2022 the model stores the annual earnings period threshold -# divided by 52 (12,570 / 52, rounded to 241.73), as SI 2001/1004 reg 11(3)(c) -# does for periods of whole weeks; 52 weeks give £12,569.96. The weekly amount -# in reg 10(c) is £242. -# Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The -# 2027-28 to 2030-31 values are announced policy (Autumn Statement 2022 and -# Budget 2025), to be set in each year's rates, limits and thresholds -# regulations. -# From 2031-32 (the tax year after the last value below) the model keeps the -# threshold at the personal allowance over 52, to the penny, as for 241.73 -# (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The -# government has kept the PT aligned with the personal allowance, which Income -# Tax Act 2007 s57 indexes by September CPI from 2031-32, and says the PT will -# be uprated by September CPI after 2030-31 (Explanatory Memorandum to SI -# 2026/231 para 5.1). That is announced policy, not law; keeping it equal to -# the allowance, round-up included, is a modelling assumption consistent with -# it. The file has no uprating. -values: - 2015-04-06: 155 - 2016-04-06: 155 - 2017-04-06: 157 - 2018-04-06: 162 - 2019-04-06: 166 - 2020-04-06: 183 - 2021-04-06: 184 - 2022-04-06: - value: 190 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2022(6) - href: https://www.legislation.gov.uk/uksi/2022/232/regulation/6/made#regulation-6-2-a - 2022-07-01: - value: 241.73 - metadata: - reference: - - title: Spring Statement 2022 - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1062486/Spring_Statement_2022_Web_Accessible.pdf - 2026-04-06: - value: 241.73 - metadata: - reference: - - title: The Social Security (Contributions) Regulations 2001 reg 10(c) and reg 11(3), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 - - title: SI 2026/231 reg 5 - href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made - 2027-04-06: - value: 241.73 - metadata: - reference: - - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" - href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 - 2028-04-06: - value: 241.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2029-04-06: - value: 241.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2030-04-06: - value: 241.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf -metadata: - label: NI primary threshold - unit: currency-GBP - period: week - reference: - - title: "GOV.UK | Rates and allowances: National Insurance contributions" - href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml deleted file mode 100644 index d5cac1d7e9..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/secondary_threshold.yaml +++ /dev/null @@ -1,103 +0,0 @@ -description: Secondary threshold for National Insurance. -metadata: - label: NI secondary threshold - period: week - reference: - - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions - unit: currency-GBP -# From 2025-26 the value is the annual £5,000 in SI 2001/1004 reg 11(3A)(b) -# divided by 52 (96.153846 = 5,000 / 52), so that the model's 52-week -# annualisation gives £5,000. This is the convention reg 11(3)(c) and -# 11(2A)(c) use for the primary threshold and upper earnings limit, which the -# model also stores as annual / 52. The rounded weekly figure in reg 10(d) is -# £96, which would annualise to £4,992 (#1967). Before 2025-26 the weekly -# figure times 52 equals the annual one (for example £175 x 52 = £9,100). -# Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The -# 2027-28 to 2030-31 values are announced policy (Autumn Budget 2024 and -# Budget 2025), to be set in each year's rates, limits and thresholds -# regulations. -# From 2031-32 (the tax year after the last value below) the model raises the -# threshold each year by September CPI of the previous year, unrounded -# (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The -# Autumn Budget 2024 note said the threshold "will be increased in line with -# Consumer Prices Index (CPI)" after its freeze; nothing later sets the rule, -# so this is a modelling assumption, and the file has no uprating. -values: - # £155 was the primary threshold in 2015-16 and 2016-17 (reg 10(c)), not the - # secondary threshold (#2178). - 2015-04-06: - value: 156 - metadata: - reference: - - title: The Social Security (Contributions) Regulations 2001 reg 10(d) (£156 a week), as at 6 April 2015 - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/10/2015-04-06 - - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£8,112 where the earnings period is a year), as at 6 April 2015 - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2015-04-06 - 2016-04-06: - value: 156 - metadata: - reference: - - title: The Social Security (Contributions) Regulations 2001 reg 10(d) (£156 a week), as at 6 April 2016 - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/10/2016-04-06 - - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£8,112 where the earnings period is a year), as at 6 April 2016 - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2016-04-06 - 2017-04-06: 157 - 2018-04-06: 162 - 2019-04-06: 166 - 2020-04-06: 169 - 2021-04-06: 170 - 2022-04-06: - metadata: - reference: - - href: https://www.legislation.gov.uk/uksi/2022/232/regulation/6/made#regulation-6-2-a - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments - and National Insurance Funds Payments) Regulations 2022(6) - value: 175 - 2024-01-01: 175 - 2025-04-06: - value: 96.153846 - metadata: - reference: - - title: National Insurance Contributions (Secondary Class 1 Contributions) Act 2025 s.2 - href: https://www.legislation.gov.uk/ukpga/2025/11/section/2 - - title: The Social Security (Contributions) Regulations 2001 reg 11(3A)(b) (£5,000 where the earnings period is a year) - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11 - 2026-04-06: - value: 96.153846 - metadata: - reference: - - title: The Social Security (Contributions) Regulations 2001 reg 10(d) and reg 11(3A), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 - - title: SI 2026/231 reg 5 - href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made - 2027-04-06: - value: 96.153846 - metadata: - reference: - - title: "HMRC: Changes to the Class 1 National Insurance Contributions Secondary Threshold, the Secondary Class 1 National Insurance contributions rate, and the Employment Allowance from 6 April 2025 (Autumn Budget 2024)" - href: https://www.gov.uk/government/publications/changes-to-the-class-1-national-insurance-contributions-secondary-threshold-the-secondary-class-1-national-insurance-contributions-rate-and-the-empl/changes-to-the-class-1-national-insurance-contributions-secondary-threshold-the-secondary-class-1-national-insurance-contributions-rate-and-the-empl - 2028-04-06: - value: 96.153846 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.112 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: Explanatory Memorandum to SI 2026/231 para 5.26 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2029-04-06: - value: 96.153846 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.112 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: Explanatory Memorandum to SI 2026/231 para 5.26 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2030-04-06: - value: 96.153846 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.112 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: Explanatory Memorandum to SI 2026/231 para 5.26 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml deleted file mode 100644 index 62ff414c47..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_1/thresholds/upper_earnings_limit.yaml +++ /dev/null @@ -1,79 +0,0 @@ -description: The Upper Earnings Limit is the upper bound for the main rate of Class 1 National Insurance. -# From 2021-22 the model stores the annual earnings period limit divided by 52 -# (50,270 / 52, rounded to 966.73), as SI 2001/1004 reg 11(2A)(c) does for -# periods of whole weeks; 52 weeks give £50,269.96. The weekly amount in -# reg 10(b) is £967. -# Legal status: SI 2001/1004 reg 10 sets the amounts for 2026-27 only. The -# 2027-28 to 2030-31 values are announced policy (Autumn Statement 2022 and -# Budget 2025), to be set in each year's rates, limits and thresholds -# regulations. -# From 2031-32 (the tax year after the last value below) the model keeps the -# limit at the higher rate threshold (personal allowance plus basic rate -# limit) over 52, to the penny, as for 966.73 -# (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The -# UEL has been aligned with the higher rate threshold since April 2009, and -# for 2026-27 "will remain at a level equivalent to the annual HRT" -# (Explanatory Memorandum to SI 2026/231 paras 5.14-5.15); para 5.1 says it -# will be uprated by September CPI after 2030-31. Income Tax Act 2007 ss21 and -# 57 index the higher rate threshold by September CPI from 2031-32. Keeping -# the UEL equal to it, round-ups included, is a modelling assumption -# consistent with that announced policy, not law. The file has no uprating. -values: - 2015-04-06: 815 - 2016-04-06: 827 - 2017-04-06: 866 - 2018-04-06: 892 - 2019-04-06: 962 - 2020-04-06: 962 - 2021-04-06: 966.73 - 2026-04-06: - value: 966.73 - metadata: - reference: - - title: The Social Security (Contributions) Regulations 2001 reg 10(b) and reg 11(2A), for 2026-27 (reg 10's tax year set by SI 2026/231 reg 5(2)(a)) - href: https://www.legislation.gov.uk/uksi/2001/1004/regulation/11/2026-04-06 - - title: SI 2026/231 reg 5 - href: https://www.legislation.gov.uk/uksi/2026/231/regulation/5/made - 2027-04-06: - value: 966.73 - metadata: - reference: - - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" - href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 - 2028-04-06: - value: 966.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2029-04-06: - value: 966.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2030-04-06: - value: 966.73 - metadata: - reference: - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf -metadata: - label: NI upper earnings limit - unit: currency-GBP - period: week - reference: - - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/contribution_scale.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/contribution_scale.yaml new file mode 100644 index 0000000000..9bd326ca1f --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/contribution_scale.yaml @@ -0,0 +1,332 @@ +description: Class 4 marginal contribution schedule. +metadata: + label: Class 4 marginal contribution schedule + threshold_unit: currency-GBP + rate_unit: /1 +brackets: + - threshold: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: currency-GBP + period: year + rate: + values: + 0001-01-01: 0 + metadata: + household: false + economy: false + unit: /1 + - threshold: + description: The Lower Profits Limit is the threshold at which self-employed earners pay the main Class 4 National Insurance rate. + # Legal status: £12,570 is the standing figure in SSCBA 1992 s.15(3)(a), which + # stays until altered by order under SSAA 1992 s.141. Budget 2025 commits to + # keeping it until April 2031. From 2031-32 (the tax year after the last value + # below) the model keeps the limit equal to the personal allowance + # (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The + # government has kept the LPL aligned with the personal allowance, which + # Income Tax Act 2007 s57 indexes by September CPI from 2031-32, and says the + # LPL will be uprated by September CPI after 2030-31 (Explanatory Memorandum + # to SI 2026/231 para 5.1). That is announced policy, not law; keeping it + # equal to the allowance, round-up included, is a modelling assumption + # consistent with it. The file has no uprating. + values: + 2015-04-06: + value: 8_060 + metadata: + reference: + - title: The Social Security (Contributions) (Re-rating and National Insurance Funds Payments) Order 2015 art 3(a) + href: https://www.legislation.gov.uk/uksi/2015/588/article/3/made + - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2016, unchanged for 2016-17) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2016-04-06 + 2017-04-06: + value: 8_164 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2017 reg 5(a) + href: https://www.legislation.gov.uk/uksi/2017/415/regulation/5/made + 2018-04-06: + value: 8_424 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2018 reg 5(a) + href: https://www.legislation.gov.uk/uksi/2018/337/regulation/5/made + 2019-04-06: + value: 8_632 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2019 reg 5(a) + href: https://www.legislation.gov.uk/uksi/2019/262/regulation/5/made + 2020-04-06: + value: 9_500 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2020 reg 5 + href: https://www.legislation.gov.uk/uksi/2020/299/regulation/5/made + 2021-04-06: + value: 9_568 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2021 reg 5(1) + href: https://www.legislation.gov.uk/uksi/2021/157/regulation/5/made + # SI 2022/232 reg 5 first set £9,880 for 2022-23; the 2022 Act replaced it + # with £11,908 for the whole year. + 2022-04-06: + value: 11_908 + metadata: + reference: + - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(1) and (2) + href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted + 2023-04-06: + value: 12_570 + metadata: + reference: + - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(3) and (4) + href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted + - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2023) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 + 2026-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + 2027-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" + href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 + 2028-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2029-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2030-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + metadata: + label: NI lower profits limit + unit: currency-GBP + period: year + reference: + - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + rate: + description: The main National Insurance rate paid between the Lower and Upper Profits Limits for self-employed profits. + values: + 2015-04-06: + value: 0.09 + metadata: + reference: + - title: National Insurance Contributions Act 2011 s 2(1)(a) (main Class 4 percentage of 9% from 6 April 2011) + href: https://www.legislation.gov.uk/ukpga/2011/3/section/2/enacted + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2015) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2015-04-06 + # Class 4 is charged on the year's profits, so 2022-23 has one rate for the + # whole tax year. The Health and Social Care Levy (Repeal) Act 2022 replaced + # the 10.25% that the Health and Social Care Levy Act 2021 s 5(2) had set. + 2022-04-06: + value: 0.0973 + metadata: + reference: + - title: Health and Social Care Levy (Repeal) Act 2022 s 2(2)(a) (main Class 4 percentage of 9.73% for the tax year 2022-23) + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + 2023-04-06: + value: 0.09 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2023) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 + 2024-04-06: + value: 0.06 + metadata: + reference: + - title: National Insurance Contributions (Reduction in Rates) Act 2024 ss 1(3)(a) and 3(1) + href: https://www.legislation.gov.uk/ukpga/2024/5/section/1/enacted + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2024) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2024-04-06 + metadata: + label: NI class 4 main rate + unit: /1 + reference: + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: "GOV.UK | Rates and allowances: National Insurance contributions" + href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + - threshold: + description: The Upper Profits Limit is the threshold at which self-employed earners pay the additional Class 4 National Insurance rate. + # Legal status: £50,270 is the standing figure in SSCBA 1992 s.15(3)(b), which + # stays until altered by order under SSAA 1992 s.141. Budget 2025 commits to + # keeping it until April 2031. From 2031-32 (the tax year after the last value + # below) the model keeps the limit equal to the higher rate threshold + # (personal allowance plus basic rate limit) + # (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The + # government has kept the UPL in line with the higher rate threshold, which + # Income Tax Act 2007 ss21 and 57 index by September CPI from 2031-32, and + # says the UPL will be uprated by September CPI after 2030-31 (Explanatory + # Memorandum to SI 2026/231 paras 5.1 and 5.25). That is announced policy, not + # law; keeping it equal to the higher rate threshold, round-ups included, is a + # modelling assumption consistent with it. The file has no uprating. + values: + 2015-04-06: + value: 42_385 + metadata: + reference: + - title: The Social Security (Contributions) (Re-rating and National Insurance Funds Payments) Order 2015 art 3(b) + href: https://www.legislation.gov.uk/uksi/2015/588/article/3/made + 2016-04-06: + value: 43_000 + metadata: + reference: + - title: The Social Security (Contributions) (Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2016 reg 2 + href: https://www.legislation.gov.uk/uksi/2016/343/regulation/2/made + 2017-04-06: + value: 45_000 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2017 reg 5(b) + href: https://www.legislation.gov.uk/uksi/2017/415/regulation/5/made + 2018-04-06: + value: 46_350 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2018 reg 5(b) + href: https://www.legislation.gov.uk/uksi/2018/337/regulation/5/made + 2019-04-06: + value: 50_000 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2019 reg 5(b) + href: https://www.legislation.gov.uk/uksi/2019/262/regulation/5/made + - title: Social Security Contributions and Benefits Act 1992 s 15(3) (as at 6 April 2020, unchanged for 2020-21) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2020-04-06 + 2021-04-06: + value: 50_270 + metadata: + reference: + - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2021 reg 5(2) + href: https://www.legislation.gov.uk/uksi/2021/157/regulation/5/made + 2026-04-06: + value: 50_270 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + 2027-04-06: + value: 50_270 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" + href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 + 2028-04-06: + value: 50_270 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2029-04-06: + value: 50_270 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + 2030-04-06: + value: 50_270 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: Budget 2025 (HC 1492) para 4.111 + href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 + - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" + href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt + - title: Explanatory Memorandum to SI 2026/231 para 5.25 + href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf + metadata: + label: NI upper profits limit + unit: currency-GBP + period: year + reference: + - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf + - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions + rate: + description: The additional National Insurance rate paid above the Upper Profits Limit for self-employed profits. + values: + 2015-04-06: + value: 0.02 + metadata: + reference: + - title: National Insurance Contributions Act 2011 s 2(1)(b) (additional Class 4 percentage of 2% from 6 April 2011) + href: https://www.legislation.gov.uk/ukpga/2011/3/section/2/enacted + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(b) (as at 6 April 2015) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2015-04-06 + # Class 4 is charged on the year's profits, so 2022-23 has one rate for the + # whole tax year. The Health and Social Care Levy (Repeal) Act 2022 replaced + # the 3.25% that the Health and Social Care Levy Act 2021 s 5(2) had set. + 2022-04-06: + value: 0.0273 + metadata: + reference: + - title: Health and Social Care Levy (Repeal) Act 2022 s 2(2)(b) (additional Class 4 percentage of 2.73% for the tax year 2022-23) + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + 2023-04-06: + value: 0.02 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(b) (as at 6 April 2023) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 + metadata: + label: NI class 4 additional rate + unit: /1 + reference: + - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 + - title: "GOV.UK | Rates and allowances: National Insurance contributions" + href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml deleted file mode 100644 index 81d3c7d5c6..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml +++ /dev/null @@ -1,33 +0,0 @@ -description: The additional National Insurance rate paid above the Upper Profits Limit for self-employed profits. -values: - 2015-04-06: - value: 0.02 - metadata: - reference: - - title: National Insurance Contributions Act 2011 s 2(1)(b) (additional Class 4 percentage of 2% from 6 April 2011) - href: https://www.legislation.gov.uk/ukpga/2011/3/section/2/enacted - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(b) (as at 6 April 2015) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2015-04-06 - # Class 4 is charged on the year's profits, so 2022-23 has one rate for the - # whole tax year. The Health and Social Care Levy (Repeal) Act 2022 replaced - # the 3.25% that the Health and Social Care Levy Act 2021 s 5(2) had set. - 2022-04-06: - value: 0.0273 - metadata: - reference: - - title: Health and Social Care Levy (Repeal) Act 2022 s 2(2)(b) (additional Class 4 percentage of 2.73% for the tax year 2022-23) - href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 - 2023-04-06: - value: 0.02 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(b) (as at 6 April 2023) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 -metadata: - label: NI class 4 additional rate - unit: /1 - reference: - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: "GOV.UK | Rates and allowances: National Insurance contributions" - href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml deleted file mode 100644 index 5f3dab92c3..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml +++ /dev/null @@ -1,41 +0,0 @@ -description: The main National Insurance rate paid between the Lower and Upper Profits Limits for self-employed profits. -values: - 2015-04-06: - value: 0.09 - metadata: - reference: - - title: National Insurance Contributions Act 2011 s 2(1)(a) (main Class 4 percentage of 9% from 6 April 2011) - href: https://www.legislation.gov.uk/ukpga/2011/3/section/2/enacted - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2015) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2015-04-06 - # Class 4 is charged on the year's profits, so 2022-23 has one rate for the - # whole tax year. The Health and Social Care Levy (Repeal) Act 2022 replaced - # the 10.25% that the Health and Social Care Levy Act 2021 s 5(2) had set. - 2022-04-06: - value: 0.0973 - metadata: - reference: - - title: Health and Social Care Levy (Repeal) Act 2022 s 2(2)(a) (main Class 4 percentage of 9.73% for the tax year 2022-23) - href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 - 2023-04-06: - value: 0.09 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2023) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 - 2024-04-06: - value: 0.06 - metadata: - reference: - - title: National Insurance Contributions (Reduction in Rates) Act 2024 ss 1(3)(a) and 3(1) - href: https://www.legislation.gov.uk/ukpga/2024/5/section/1/enacted - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA)(a) (as at 6 April 2024) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2024-04-06 -metadata: - label: NI class 4 main rate - unit: /1 - reference: - - title: Social Security Contributions and Benefits Act 1992 s 15(3ZA) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: "GOV.UK | Rates and allowances: National Insurance contributions" - href: https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml deleted file mode 100644 index ffb02e530a..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml +++ /dev/null @@ -1,124 +0,0 @@ -description: The Lower Profits Limit is the threshold at which self-employed earners pay the main Class 4 National Insurance rate. -# Legal status: £12,570 is the standing figure in SSCBA 1992 s.15(3)(a), which -# stays until altered by order under SSAA 1992 s.141. Budget 2025 commits to -# keeping it until April 2031. From 2031-32 (the tax year after the last value -# below) the model keeps the limit equal to the personal allowance -# (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The -# government has kept the LPL aligned with the personal allowance, which -# Income Tax Act 2007 s57 indexes by September CPI from 2031-32, and says the -# LPL will be uprated by September CPI after 2030-31 (Explanatory Memorandum -# to SI 2026/231 para 5.1). That is announced policy, not law; keeping it -# equal to the allowance, round-up included, is a modelling assumption -# consistent with it. The file has no uprating. -values: - 2015-04-06: - value: 8_060 - metadata: - reference: - - title: The Social Security (Contributions) (Re-rating and National Insurance Funds Payments) Order 2015 art 3(a) - href: https://www.legislation.gov.uk/uksi/2015/588/article/3/made - - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2016, unchanged for 2016-17) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2016-04-06 - 2017-04-06: - value: 8_164 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2017 reg 5(a) - href: https://www.legislation.gov.uk/uksi/2017/415/regulation/5/made - 2018-04-06: - value: 8_424 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2018 reg 5(a) - href: https://www.legislation.gov.uk/uksi/2018/337/regulation/5/made - 2019-04-06: - value: 8_632 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2019 reg 5(a) - href: https://www.legislation.gov.uk/uksi/2019/262/regulation/5/made - 2020-04-06: - value: 9_500 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2020 reg 5 - href: https://www.legislation.gov.uk/uksi/2020/299/regulation/5/made - 2021-04-06: - value: 9_568 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2021 reg 5(1) - href: https://www.legislation.gov.uk/uksi/2021/157/regulation/5/made - # SI 2022/232 reg 5 first set £9,880 for 2022-23; the 2022 Act replaced it - # with £11,908 for the whole year. - 2022-04-06: - value: 11_908 - metadata: - reference: - - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(1) and (2) - href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted - 2023-04-06: - value: 12_570 - metadata: - reference: - - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(3) and (4) - href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted - - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2023) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 - 2026-04-06: - value: 12_570 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - 2027-04-06: - value: 12_570 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" - href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 - 2028-04-06: - value: 12_570 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2029-04-06: - value: 12_570 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2030-04-06: - value: 12_570 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf -metadata: - label: NI lower profits limit - unit: currency-GBP - period: year - reference: - - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml deleted file mode 100644 index 74e475204d..0000000000 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/upper_profits_limit.yaml +++ /dev/null @@ -1,109 +0,0 @@ -description: The Upper Profits Limit is the threshold at which self-employed earners pay the additional Class 4 National Insurance rate. -# Legal status: £50,270 is the standing figure in SSCBA 1992 s.15(3)(b), which -# stays until altered by order under SSAA 1992 s.141. Budget 2025 commits to -# keeping it until April 2031. From 2031-32 (the tax year after the last value -# below) the model keeps the limit equal to the higher rate threshold -# (personal allowance plus basic rate limit) -# (policyengine_uk/parameters/gov/hmrc/create_threshold_indexation.py). The -# government has kept the UPL in line with the higher rate threshold, which -# Income Tax Act 2007 ss21 and 57 index by September CPI from 2031-32, and -# says the UPL will be uprated by September CPI after 2030-31 (Explanatory -# Memorandum to SI 2026/231 paras 5.1 and 5.25). That is announced policy, not -# law; keeping it equal to the higher rate threshold, round-ups included, is a -# modelling assumption consistent with it. The file has no uprating. -values: - 2015-04-06: - value: 42_385 - metadata: - reference: - - title: The Social Security (Contributions) (Re-rating and National Insurance Funds Payments) Order 2015 art 3(b) - href: https://www.legislation.gov.uk/uksi/2015/588/article/3/made - 2016-04-06: - value: 43_000 - metadata: - reference: - - title: The Social Security (Contributions) (Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2016 reg 2 - href: https://www.legislation.gov.uk/uksi/2016/343/regulation/2/made - 2017-04-06: - value: 45_000 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2017 reg 5(b) - href: https://www.legislation.gov.uk/uksi/2017/415/regulation/5/made - 2018-04-06: - value: 46_350 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2018 reg 5(b) - href: https://www.legislation.gov.uk/uksi/2018/337/regulation/5/made - 2019-04-06: - value: 50_000 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2019 reg 5(b) - href: https://www.legislation.gov.uk/uksi/2019/262/regulation/5/made - - title: Social Security Contributions and Benefits Act 1992 s 15(3) (as at 6 April 2020, unchanged for 2020-21) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2020-04-06 - 2021-04-06: - value: 50_270 - metadata: - reference: - - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2021 reg 5(2) - href: https://www.legislation.gov.uk/uksi/2021/157/regulation/5/made - 2026-04-06: - value: 50_270 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - 2027-04-06: - value: 50_270 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: "HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)" - href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028 - 2028-04-06: - value: 50_270 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2029-04-06: - value: 50_270 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf - 2030-04-06: - value: 50_270 - metadata: - reference: - - title: Social Security Contributions and Benefits Act 1992 s.15(3)(b) - href: https://www.legislation.gov.uk/ukpga/1992/4/section/15 - - title: Budget 2025 (HC 1492) para 4.111 - href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113 - - title: "HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031" - href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt - - title: Explanatory Memorandum to SI 2026/231 para 5.25 - href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf -metadata: - label: NI upper profits limit - unit: currency-GBP - period: year - reference: - - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf - - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions diff --git a/policyengine_uk/parameters/gov/simulation/labour_supply_responses/aliases.py b/policyengine_uk/parameters/gov/simulation/labour_supply_responses/aliases.py index d88f3a7616..c2108e39c5 100644 --- a/policyengine_uk/parameters/gov/simulation/labour_supply_responses/aliases.py +++ b/policyengine_uk/parameters/gov/simulation/labour_supply_responses/aliases.py @@ -10,6 +10,7 @@ """ from policyengine_core.parameters import ParameterNode +from policyengine_uk.utils.ni_parameter_scales import canonicalize_ni_parameter_path _LEGACY_LSR_PATH = "gov.simulation.labor_supply_responses" @@ -35,7 +36,7 @@ def canonicalize_parameter_changes(changes: dict) -> dict: """ canonical = {} for path, value in changes.items(): - name = canonicalize_lsr_parameter_path(path) + name = canonicalize_ni_parameter_path(canonicalize_lsr_parameter_path(path)) if name not in canonical: canonical[name] = value elif isinstance(canonical[name], dict) and isinstance(value, dict): diff --git a/policyengine_uk/tax_benefit_system.py b/policyengine_uk/tax_benefit_system.py index 9d49758d48..1c5565ebb5 100644 --- a/policyengine_uk/tax_benefit_system.py +++ b/policyengine_uk/tax_benefit_system.py @@ -79,6 +79,15 @@ convert_to_fiscal_year_parameters, ) +from policyengine_uk.utils.ni_parameter_scales import ( + LegacyNIParameter, + canonical_descendants, + canonical_ni_processing, + validate_ni_scale_compatibility, + canonical_ni_uprating_references, + canonicalize_ni_parameter_path, +) + logger = logging.getLogger(__name__) @@ -184,6 +193,7 @@ def process_parameter_tree( written just after the step that generates it (see ``_add_generated_parameters``). """ + validate_ni_scale_compatibility(parameters) parameters = _add_generated_parameters(parameters, generated) # Create baseline parameters for reform comparisons @@ -196,8 +206,10 @@ def process_parameter_tree( parameters.add_child("baseline", baseline) # Apply general parameter operations - parameters = propagate_parameter_metadata(parameters) - parameters = uprate_parameters(parameters) + with canonical_ni_processing(parameters): + propagate_parameter_metadata(parameters) + with canonical_ni_uprating_references(parameters): + uprate_parameters(parameters) parameters = backdate_parameters(parameters, BACKDATED_FROM) # Prior law's projected path for every parameter a reform can change: the # gov tree, before annualisation, and the trees outside it (household), @@ -248,7 +260,8 @@ def neutralize_variable(self, variable_name: str) -> None: def reset_parameter_caches(self): """Clear dated views through their Core owners, retaining the tree.""" - for node in [self.parameters, *self.parameters.get_descendants()]: + validate_ni_scale_compatibility(self.parameters) + for node in [self.parameters, *canonical_descendants(self.parameters)]: if isinstance(node, ParameterNode): # The public operation clears this node and its ancestors; # root-only clearing would leave cached descendants behind. @@ -569,6 +582,8 @@ def _leaf_paths(node: ParameterNode, prefix: str = "") -> List[str]: paths = [] for key, child in node.children.items(): path = f"{prefix}.{key}" if prefix else key + if isinstance(child, LegacyNIParameter): + continue if isinstance(child, Parameter): paths.append(path) elif isinstance(child, ParameterNode): @@ -593,7 +608,9 @@ def generated_parameter_defines() -> Callable[[str], bool]: _generated_paths_cache = frozenset( path for path in _leaf_paths(generated) - if not statutory(path) and canonicalize_lsr_parameter_path(path) == path + if not statutory(path) + and canonicalize_ni_parameter_path(canonicalize_lsr_parameter_path(path)) + == path ) return _generated_paths_cache.__contains__ diff --git a/policyengine_uk/tests/fixtures/ni_legacy_parameter_metadata.json b/policyengine_uk/tests/fixtures/ni_legacy_parameter_metadata.json new file mode 100644 index 0000000000..7f556e97e8 --- /dev/null +++ b/policyengine_uk/tests/fixtures/ni_legacy_parameter_metadata.json @@ -0,0 +1,266 @@ +{ + "api_commit": "71dba65b3780f0bb4699d4dcbfe179e5f1506c78", + "extraction": "Exact source-extracted pinned API method and exact helper against unmodified production before migration. Not the installed/full API application.", + "parameters": { + "gov.hmrc.national_insurance.class_1.rates.employee.additional": { + "description": "The Class 1 National Insurance additional rate is paid on employment earnings above the Upper Earnings Limit.", + "economy": true, + "household": true, + "label": "NI class 1 additional rate", + "parameter": "gov.hmrc.national_insurance.class_1.rates.employee.additional", + "period": null, + "type": "parameter", + "unit": "/1", + "values": { + "2015-01-01": 0.02, + "2015-04-01": 0.02, + "2022-04-01": 0.0325, + "2024-04-04": 0.02 + } + }, + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "description": "The Class 1 National Insurance main rate is paid on employment earnings between the Primary Threshold and the Upper Earnings Limit.", + "economy": true, + "household": true, + "label": "NI class 1 main rate", + "parameter": "gov.hmrc.national_insurance.class_1.rates.employee.main", + "period": null, + "type": "parameter", + "unit": "/1", + "values": { + "2015-01-01": 0.12, + "2015-04-01": 0.12, + "2022-04-06": 0.1325, + "2022-11-06": 0.12, + "2024-01-01": 0.1, + "2024-04-01": 0.08 + } + }, + "gov.hmrc.national_insurance.class_1.rates.employer": { + "description": "National Insurance contribution rate by employers on earnings above the Secondary Threshold.", + "economy": true, + "household": true, + "label": "NI employer rate", + "parameter": "gov.hmrc.national_insurance.class_1.rates.employer", + "period": null, + "type": "parameter", + "unit": "/1", + "values": { + "2015-01-01": 0.138, + "2015-04-01": 0.138, + "2022-04-06": 0.1505, + "2022-11-06": 0.138, + "2025-01-01": 0.15 + } + }, + "gov.hmrc.national_insurance.class_1.thresholds.primary_threshold": { + "description": "The Primary Threshold is the lower bound for the main rate of Class 1 National Insurance.", + "economy": true, + "household": true, + "label": "NI primary threshold", + "parameter": "gov.hmrc.national_insurance.class_1.thresholds.primary_threshold", + "period": "week", + "type": "parameter", + "unit": "currency-GBP", + "values": { + "2015-01-01": 155, + "2015-04-06": 155, + "2016-04-06": 155, + "2017-04-06": 157, + "2018-04-06": 162, + "2019-04-06": 166, + "2020-04-06": 183, + "2021-04-06": 184, + "2022-04-06": 190, + "2022-07-01": 241.73, + "2026-04-06": 241.73, + "2027-04-06": 241.73, + "2028-04-06": 241.73, + "2029-04-06": 241.73, + "2030-04-06": 241.73, + "2031-01-01": 246.73, + "2032-01-01": 251.73, + "2033-01-01": 256.92, + "2034-01-01": 262.12, + "2035-01-01": 267.5, + "2036-01-01": 272.88, + "2037-01-01": 278.46, + "2038-01-01": 284.04, + "2039-01-01": 289.81 + } + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "description": "Secondary threshold for National Insurance.", + "economy": true, + "household": true, + "label": "NI secondary threshold", + "parameter": "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold", + "period": "week", + "type": "parameter", + "unit": "currency-GBP", + "values": { + "2015-01-01": 156, + "2015-04-06": 156, + "2016-04-06": 156, + "2017-04-06": 157, + "2018-04-06": 162, + "2019-04-06": 166, + "2020-04-06": 169, + "2021-04-06": 170, + "2022-04-06": 175, + "2024-01-01": 175, + "2025-04-06": 96.153846, + "2026-04-06": 96.153846, + "2027-04-06": 96.153846, + "2028-04-06": 96.153846, + "2029-04-06": 96.153846, + "2030-04-06": 96.153846, + "2031-01-01": 98.07692292, + "2032-01-01": 100.0384613784, + "2033-01-01": 102.039230605968, + "2034-01-01": 104.08001521808735, + "2035-01-01": 106.16161552244911, + "2036-01-01": 108.2848478328981, + "2037-01-01": 110.45054478955605, + "2038-01-01": 112.65955568534717, + "2039-01-01": 114.91274679905412 + } + }, + "gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit": { + "description": "The Upper Earnings Limit is the upper bound for the main rate of Class 1 National Insurance.", + "economy": true, + "household": true, + "label": "NI upper earnings limit", + "parameter": "gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit", + "period": "week", + "type": "parameter", + "unit": "currency-GBP", + "values": { + "2015-01-01": 815, + "2015-04-06": 815, + "2016-04-06": 827, + "2017-04-06": 866, + "2018-04-06": 892, + "2019-04-06": 962, + "2020-04-06": 962, + "2021-04-06": 966.73, + "2026-04-06": 966.73, + "2027-04-06": 966.73, + "2028-04-06": 966.73, + "2029-04-06": 966.73, + "2030-04-06": 966.73, + "2031-01-01": 987.12, + "2032-01-01": 1007.5, + "2033-01-01": 1028.08, + "2034-01-01": 1050.58, + "2035-01-01": 1073.27, + "2036-01-01": 1095.96, + "2037-01-01": 1118.85, + "2038-01-01": 1141.73, + "2039-01-01": 1164.81 + } + }, + "gov.hmrc.national_insurance.class_4.rates.additional": { + "description": "The additional National Insurance rate paid above the Upper Profits Limit for self-employed profits.", + "economy": true, + "household": true, + "label": "NI class 4 additional rate", + "parameter": "gov.hmrc.national_insurance.class_4.rates.additional", + "period": null, + "type": "parameter", + "unit": "/1", + "values": { + "2015-01-01": 0.02, + "2015-04-06": 0.02, + "2022-04-06": 0.0273, + "2023-04-06": 0.02 + } + }, + "gov.hmrc.national_insurance.class_4.rates.main": { + "description": "The main National Insurance rate paid between the Lower and Upper Profits Limits for self-employed profits.", + "economy": true, + "household": true, + "label": "NI class 4 main rate", + "parameter": "gov.hmrc.national_insurance.class_4.rates.main", + "period": null, + "type": "parameter", + "unit": "/1", + "values": { + "2015-01-01": 0.09, + "2015-04-06": 0.09, + "2022-04-06": 0.0973, + "2023-04-06": 0.09, + "2024-04-06": 0.06 + } + }, + "gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit": { + "description": "The Lower Profits Limit is the threshold at which self-employed earners pay the main Class 4 National Insurance rate.", + "economy": true, + "household": true, + "label": "NI lower profits limit", + "parameter": "gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit", + "period": "year", + "type": "parameter", + "unit": "currency-GBP", + "values": { + "2015-01-01": 8060, + "2015-04-06": 8060, + "2017-04-06": 8164, + "2018-04-06": 8424, + "2019-04-06": 8632, + "2020-04-06": 9500, + "2021-04-06": 9568, + "2022-04-06": 11908, + "2023-04-06": 12570, + "2026-04-06": 12570, + "2027-04-06": 12570, + "2028-04-06": 12570, + "2029-04-06": 12570, + "2030-04-06": 12570, + "2031-01-01": 12830.0, + "2032-01-01": 13090.0, + "2033-01-01": 13360.0, + "2034-01-01": 13630.0, + "2035-01-01": 13910.0, + "2036-01-01": 14190.0, + "2037-01-01": 14480.0, + "2038-01-01": 14770.0, + "2039-01-01": 15070.0 + } + }, + "gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit": { + "description": "The Upper Profits Limit is the threshold at which self-employed earners pay the additional Class 4 National Insurance rate.", + "economy": true, + "household": true, + "label": "NI upper profits limit", + "parameter": "gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit", + "period": "year", + "type": "parameter", + "unit": "currency-GBP", + "values": { + "2015-01-01": 42385, + "2015-04-06": 42385, + "2016-04-06": 43000, + "2017-04-06": 45000, + "2018-04-06": 46350, + "2019-04-06": 50000, + "2021-04-06": 50270, + "2026-04-06": 50270, + "2027-04-06": 50270, + "2028-04-06": 50270, + "2029-04-06": 50270, + "2030-04-06": 50270, + "2031-01-01": 51330.0, + "2032-01-01": 52390.0, + "2033-01-01": 53460.0, + "2034-01-01": 54630.0, + "2035-01-01": 55810.0, + "2036-01-01": 56990.0, + "2037-01-01": 58180.0, + "2038-01-01": 59370.0, + "2039-01-01": 60570.0 + } + } + }, + "source_base": "e1601dbe4feb15e71d0d59b7a57f9d860741c068" +} diff --git a/policyengine_uk/tests/fixtures/ni_metadata_api_contract.py b/policyengine_uk/tests/fixtures/ni_metadata_api_contract.py new file mode 100644 index 0000000000..77b9c3af9f --- /dev/null +++ b/policyengine_uk/tests/fixtures/ni_metadata_api_contract.py @@ -0,0 +1,179 @@ +"""Source-extracted API contract, not an installed/full API execution. + +Country.build_parameters and get_safe_json are copied without body changes from +PolicyEngine/policyengine-api commit 71dba65b3780f0bb4699d4dcbfe179e5f1506c78. +country.py SHA256 cfbb900691d45bd95e05b08e53bf72708ac75ee82d1647c04ff09240a92e61bb; +utils/json.py SHA256 acef75a37b1d90cc52c0bda57db47ffd2348eaa400d4674d244c8090b47ddc61. +https://github.com/PolicyEngine/policyengine-api/blob/71dba65b3780f0bb4699d4dcbfe179e5f1506c78/policyengine_api/country.py#L371 +https://github.com/PolicyEngine/policyengine-api/blob/71dba65b3780f0bb4699d4dcbfe179e5f1506c78/policyengine_api/utils/json.py#L34 +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from policyengine_core.taxbenefitsystems import TaxBenefitSystem + +if TYPE_CHECKING: + from policyengine_api.json_types import JSONValue + +from policyengine_core.parameters.operations.get_parameter import get_parameter +from policyengine_core.periods import instant +from policyengine_core.reforms import Reform +from policyengine_core.parameters import ( + Parameter, + ParameterNode, + ParameterScale, + ParameterScaleBracket, +) + + +def get_safe_json(value: object) -> JSONValue: + # This function is used when constructing metadata, + # which will be consumed by JS, hence Infinity, not .inf + if isinstance(value, (int, float)): + if value == np.inf: + return "Infinity" + if value == -np.inf: + return "-Infinity" + return value + if isinstance(value, str): + return value + if isinstance(value, dict): + return {k: get_safe_json(v) for k, v in value.items()} + if isinstance(value, list): + return [get_safe_json(v) for v in value] + return None + + +def build_parameters(self) -> dict: + parameters = self.tax_benefit_system.parameters + parameter_data = {} + for parameter in parameters.get_descendants(): + if "gov" != parameter.name[:3]: + continue + end_name = parameter.name.split(".")[-1] + if isinstance(parameter, ParameterScale): + parameter.propagate_units() + parameter_data[parameter.name] = { + "type": "parameterNode", + "parameter": parameter.name, + "description": parameter.description, + "label": parameter.metadata.get("label", end_name.replace("_", " ")), + } + elif isinstance(parameter, ParameterScaleBracket): + bracket_index = int(parameter.name[parameter.name.index("[") + 1 : -1]) + # Set the label to 'first bracket' for the first bracket, 'second bracket' for the second, etc. + bracket_label = f"bracket {bracket_index + 1}" + parameter_data[parameter.name] = { + "type": "parameterNode", + "parameter": parameter.name, + "description": parameter.description, + "label": parameter.metadata.get("label", bracket_label), + } + elif isinstance(parameter, Parameter): + parameter_data[parameter.name] = { + "type": "parameter", + "parameter": parameter.name, + "description": parameter.description, + "label": parameter.metadata.get("label", end_name.replace("_", " ")), + "unit": parameter.metadata.get("unit"), + "period": parameter.metadata.get("period"), + "values": { + value_at_instant.instant_str: get_safe_json(value_at_instant.value) + for value_at_instant in parameter.values_list + }, + "economy": parameter.metadata.get("economy", True), + "household": parameter.metadata.get("household", True), + } + elif isinstance(parameters, ParameterNode): + parameter_data[parameter.name] = { + "type": "parameterNode", + "parameter": parameter.name, + "description": parameter.description, + "label": parameter.metadata.get("label", end_name.replace("_", " ")), + "economy": parameter.metadata.get("economy", True), + "household": parameter.metadata.get("household", True), + } + return parameter_data + + +def _normalize_reform_values( + self, + reform: dict, + system: TaxBenefitSystem, +) -> dict: + normalized_reform = {} + for parameter_name, parameter_updates in reform.items(): + parameter = get_parameter(system.parameters, parameter_name) + normalized_reform[parameter_name] = {} + for time_period, value in parameter_updates.items(): + node_type = type(parameter.values_list[-1].value) + if node_type is int: + node_type = float + try: + value = float(value) + except Exception: + pass + normalized_reform[parameter_name][time_period] = node_type(value) + return normalized_reform + + +def _apply_reform_to_system( + self, + system: TaxBenefitSystem, + reform: dict, +) -> None: + for parameter_name, parameter_updates in reform.items(): + parameter = get_parameter(system.parameters, parameter_name) + for time_period, value in parameter_updates.items(): + start_instant, end_instant = time_period.split(".") + parameter.update( + start=instant(start_instant), + stop=instant(end_instant), + value=value, + ) + + +def create_policy_reform(policy_data: dict) -> dict: + """ + Create a policy reform. + + Args: + policy_data (dict): The policy data. + + Returns: + dict: The reform. + """ + + def modify_parameters(parameters: ParameterNode) -> ParameterNode: + for path, values in policy_data.items(): + node = parameters + for step in path.split("."): + if "[" in step: + step, index = step.split("[") + index = int(index[:-1]) + node = node.children[step].brackets[index] + else: + node = node.children[step] + for period, value in values.items(): + start, end = period.split(".") + node_type = type(node.values_list[-1].value) + if node_type is int: + node_type = float # '0' is of type int by default, but usually we want to cast to float. + if node.values_list[-1].value is None: + node_type = float + node.update( + start=instant(start), + stop=instant(end), + value=node_type(value), + ) + + return parameters + + class reform(Reform): + def apply(self) -> None: + self.modify_parameters(modify_parameters) + + return reform diff --git a/policyengine_uk/tests/test_ni_health_and_social_care_levy.py b/policyengine_uk/tests/test_ni_health_and_social_care_levy.py index d0cbd4d53f..e240128b47 100644 --- a/policyengine_uk/tests/test_ni_health_and_social_care_levy.py +++ b/policyengine_uk/tests/test_ni_health_and_social_care_levy.py @@ -53,10 +53,9 @@ / "hmrc" / "national_insurance" / "class_1" - / "rates" ) -EMPLOYER_FILE = RATES / "employer.yaml" -EMPLOYEE_MAIN_FILE = RATES / "employee" / "main.yaml" +EMPLOYER_FILE = RATES / "employer_scale.yaml" +EMPLOYEE_MAIN_FILE = RATES / "employee_scale.yaml" LEVY_START = datetime.date(2022, 4, 6) # HSCL Act 2021 s.5(1): tax year 2022-23 LEVY_END = datetime.date(2022, 11, 6) # Repeal Act 2022 s.2(1) @@ -72,12 +71,12 @@ @pytest.fixture(scope="module") def employer(): - return load_parameter_file(str(EMPLOYER_FILE), "employer") + return load_parameter_file(str(EMPLOYER_FILE), "employer").brackets[1].rate @pytest.fixture(scope="module") def employee_main(): - return load_parameter_file(str(EMPLOYEE_MAIN_FILE), "main") + return load_parameter_file(str(EMPLOYEE_MAIN_FILE), "main").brackets[1].rate @pytest.fixture(scope="module") diff --git a/policyengine_uk/tests/test_ni_metadata_compat.py b/policyengine_uk/tests/test_ni_metadata_compat.py new file mode 100644 index 0000000000..4d0f27f1d8 --- /dev/null +++ b/policyengine_uk/tests/test_ni_metadata_compat.py @@ -0,0 +1,141 @@ +"""Pinned source-extracted API contracts; no installed API or service calls.""" + +from types import SimpleNamespace +from pathlib import Path +from policyengine_core.parameters.operations.get_parameter import get_parameter +import json +import pytest + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.tests.fixtures.ni_metadata_api_contract import ( + build_parameters, + _normalize_reform_values, + _apply_reform_to_system, + create_policy_reform, + get_safe_json, +) + +OLD = "gov.hmrc.national_insurance.class_1.rates.employee.main" +NEW = "gov.hmrc.national_insurance.class_1.employee_scale[1].rate" + + +@pytest.mark.parametrize("path", [OLD, NEW]) +def test_actual_core_get_parameter_api_normalization_and_update(path, record_property): + original = CountryTaxBenefitSystem() + system = original.clone() + data = {path: {"2026-01-01.2026-12-31": "0.16"}} + normalized = _normalize_reform_values(SimpleNamespace(), data, system) + assert normalized[path]["2026-01-01.2026-12-31"] == 0.16 + build_parameters(SimpleNamespace(tax_benefit_system=system)) + _apply_reform_to_system(SimpleNamespace(), system, normalized) + actual = build_parameters(SimpleNamespace(tax_benefit_system=system)) + # Exact immutable-B control reproduces baseline-name overwrite in this + # synthetic reserialization: construction-cached Country metadata is not + # a live changed-system export. Preserve that whole old record, rather + # than introducing an unrelated baseline/API lifecycle change. + # Base-control result SHA25645bc659d1915a86afb6340ef276e535588783f88a46218b993f4cb3c97a14c02. + expected = json.loads( + ( + Path(__file__).parent / "fixtures/ni_legacy_parameter_metadata.json" + ).read_text() + )["parameters"][OLD] + assert actual[OLD] == {**expected, "household": False, "economy": False} + assert actual[NEW] == {**expected, "parameter": NEW} + alias = get_parameter(system.parameters, OLD) + canonical = get_parameter(system.parameters, NEW) + assert alias.metadata is canonical.metadata + assert alias.values_list is canonical.values_list + assert alias("2026") == canonical("2026") == 0.16 + assert any( + entry.instant_str == "2026-01-01" and entry.value == 0.16 + for entry in alias.values_list + ) + assert system.parameters.baseline.get_child(OLD)("2026") == 0.08 + record_property( + "source_extracted_metadata_contract", + json.dumps( + { + "update_path": path, + "old_complete_record": actual[OLD], + "canonical_complete_record": actual[NEW], + "live_root_value": alias("2026"), + "baseline_value": system.parameters.baseline.get_child(OLD)("2026"), + "same_values_list": alias.values_list is canonical.values_list, + "same_metadata": alias.metadata is canonical.metadata, + }, + sort_keys=True, + ), + ) + assert original.parameters.get_child(OLD)("2026") == 0.08 + json.dumps(actual, allow_nan=False) + + +@pytest.mark.parametrize("path", [OLD, NEW]) +def test_actual_legacy_api_manual_children_reform_route(path): + original = CountryTaxBenefitSystem() + reform = create_policy_reform({path: {"2026-01-01.2026-12-31": 0.14}}) + updated = reform(original) + assert updated.parameters.get_child(OLD)("2026") == 0.14 + assert updated.parameters.get_child(NEW)("2026") == 0.14 + assert original.parameters.get_child(OLD)("2026") == 0.08 + + +def test_exact_pinned_json_helper_handles_nonfinite_nested_values(): + assert get_safe_json({"value": [float("inf"), float("-inf"), None, 0.08]}) == { + "value": ["Infinity", "-Infinity", None, 0.08] + } + + +# Only these discovery flags intentionally differ from the frozen H record. +HIDDEN_DISCOVERY_PATHS = ( + "gov.hmrc.national_insurance.class_1.rates.employee.main", + "gov.hmrc.national_insurance.class_1.rates.employee.additional", + "gov.hmrc.national_insurance.class_1.thresholds.primary_threshold", + "gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit", + "gov.hmrc.national_insurance.class_1.rates.employer", + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold", + "gov.hmrc.national_insurance.class_4.rates.main", + "gov.hmrc.national_insurance.class_4.rates.additional", + "gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit", + "gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit", + "gov.hmrc.national_insurance.class_1.employee_scale[0].threshold", + "gov.hmrc.national_insurance.class_1.employee_scale[0].rate", + "gov.hmrc.national_insurance.class_1.employer_scale[0].threshold", + "gov.hmrc.national_insurance.class_1.employer_scale[0].rate", + "gov.hmrc.national_insurance.class_4.contribution_scale[0].threshold", + "gov.hmrc.national_insurance.class_4.contribution_scale[0].rate", +) + + +@pytest.mark.parametrize("path", HIDDEN_DISCOVERY_PATHS) +def test_fixed_zero_and_legacy_discovery_controls_are_hidden(path): + actual = build_parameters( + SimpleNamespace(tax_benefit_system=CountryTaxBenefitSystem()) + ) + assert actual[path]["household"] is False + assert actual[path]["economy"] is False + + +def test_discovery_views_do_not_hide_or_mutate_canonical_controls(): + from policyengine_uk.utils.ni_parameter_scales import NI_LEGACY_PATHS + + system = CountryTaxBenefitSystem() + records = build_parameters(SimpleNamespace(tax_benefit_system=system)) + # Scope identity to the current gov tree, excluding the named baseline copy. + discovered = {item.name: item for item in system.parameters.gov.get_descendants()} + for old, new in NI_LEGACY_PATHS.items(): + alias = get_parameter(system.parameters, old) + canonical = get_parameter(system.parameters, new) + assert alias.metadata is canonical.metadata + assert alias.values_list is canonical.values_list + assert records[new]["household"] is True + assert records[new]["economy"] is True + assert records[old] == { + **records[new], + "parameter": old, + "household": False, + "economy": False, + } + assert discovered[old] is not alias + assert discovered[old].metadata is not canonical.metadata + assert discovered[new] is canonical diff --git a/policyengine_uk/tests/test_ni_parameter_scales.py b/policyengine_uk/tests/test_ni_parameter_scales.py new file mode 100644 index 0000000000..de913b6d12 --- /dev/null +++ b/policyengine_uk/tests/test_ni_parameter_scales.py @@ -0,0 +1,558 @@ +"""Public NI scale and legacy leaf contracts, specified before migration.""" + +import json +from pathlib import Path + +import pytest +from policyengine_core.parameters import Parameter, ParameterScale + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.tests.fixtures.ni_metadata_api_contract import build_parameters +from types import SimpleNamespace + +PREFIX = "gov.hmrc.national_insurance." +LEAVES = { + "class_1.rates.employee.main": "class_1.employee_scale[1].rate", + "class_1.rates.employee.additional": "class_1.employee_scale[2].rate", + "class_1.thresholds.primary_threshold": "class_1.employee_scale[1].threshold", + "class_1.thresholds.upper_earnings_limit": "class_1.employee_scale[2].threshold", + "class_1.rates.employer": "class_1.employer_scale[1].rate", + "class_1.thresholds.secondary_threshold": "class_1.employer_scale[1].threshold", + "class_4.rates.main": "class_4.contribution_scale[1].rate", + "class_4.rates.additional": "class_4.contribution_scale[2].rate", + "class_4.thresholds.lower_profits_limit": "class_4.contribution_scale[1].threshold", + "class_4.thresholds.upper_profits_limit": "class_4.contribution_scale[2].threshold", +} + + +def test_canonical_marginal_schedules_are_discoverable(): + parameters = CountryTaxBenefitSystem().parameters + for path in [ + "class_1.employee_scale", + "class_1.employer_scale", + "class_4.contribution_scale", + ]: + assert isinstance(parameters.get_child(PREFIX + path), ParameterScale) + + +@pytest.mark.parametrize("legacy,canonical", LEAVES.items()) +def test_ten_legacy_scalar_paths_resolve_to_canonical_leaves(legacy, canonical): + parameters = CountryTaxBenefitSystem().parameters + assert parameters.get_child(PREFIX + legacy) is parameters.get_child( + PREFIX + canonical + ) + + +def test_legacy_callable_attribute_updates_reach_canonical_leaves(): + system = CountryTaxBenefitSystem() + tree = system.parameters.clone() + tree("2026") # Warm canonical and sibling parent reads. + old_parent = tree.gov.hmrc.national_insurance.class_1.rates.employee + old_parent("2026") + old_parent.main.update(period="2026", value=0.19) + assert tree.get_child(PREFIX + "class_1.employee_scale[1].rate")("2026") == 0.19 + assert tree("2026").gov.hmrc.national_insurance.class_1.rates.employee.main == 0.19 + assert old_parent("2026").main == 0.19 + assert ( + system.parameters.get_child(PREFIX + "class_1.rates.employee.main")("2026") + == 0.08 + ) + + +def test_live_parameter_metadata_preserves_legacy_scalar_records(): + expected = json.loads( + ( + Path(__file__).parent / "fixtures/ni_legacy_parameter_metadata.json" + ).read_text() + )["parameters"] + actual = build_parameters( + SimpleNamespace(tax_benefit_system=CountryTaxBenefitSystem()) + ) + assert {name: actual[name] for name in expected} == { + name: {**record, "household": False, "economy": False} + for name, record in expected.items() + } + json.dumps(actual, allow_nan=False) + + +def test_clone_and_baseline_views_resolve_within_their_own_tree(): + source = CountryTaxBenefitSystem().parameters + clone = source.clone() + source_value = source.gov.hmrc.national_insurance.class_1.rates.employee.main( + "2026" + ) + clone.gov.hmrc.national_insurance.class_1.rates.employee.main.update( + period="2026", value=0.17 + ) + assert clone.get_child(PREFIX + "class_1.employee_scale[1].rate")("2026") == 0.17 + assert ( + source.gov.hmrc.national_insurance.class_1.rates.employee.main("2026") + == source_value + ) + assert ( + clone.baseline.gov.hmrc.national_insurance.class_1.rates.employee.main("2026") + == source_value + ) + + +def test_canonical_leaves_have_one_authoritative_registered_occurrence(): + tree = CountryTaxBenefitSystem().parameters + canonical_names = [PREFIX + name for name in LEAVES.values()] + descendants = list(tree.gov.get_descendants()) + for name in canonical_names: + canonical = tree.get_child(name) + assert isinstance(canonical, Parameter) + assert sum(item is canonical for item in descendants) == 1 + + +def test_standalone_legacy_clone_is_independent(): + tree = CountryTaxBenefitSystem().parameters + leaf = tree.gov.hmrc.national_insurance.class_1.rates.employee.main + independent = leaf.clone() + independent.update(period="2026", value=0.18) + assert leaf("2026") == 0.08 + assert independent("2026") == 0.18 + + +def test_processing_iteration_and_error_restoration_preserve_real_identity(): + from policyengine_core.parameters.operations.get_parameter import get_parameter + from policyengine_uk.utils.ni_parameter_scales import ( + LegacyNIParameter, + canonical_ni_processing, + ) + + tree = CountryTaxBenefitSystem().parameters.clone() + legacy_parent = tree.gov.hmrc.national_insurance.class_1.rates.employee + original_children = legacy_parent.children + legacy = original_children["main"] + canonical = tree.get_child(PREFIX + LEAVES["class_1.rates.employee.main"]) + canonical_parent = canonical.parent + before = build_parameters( + SimpleNamespace(tax_benefit_system=SimpleNamespace(parameters=tree)) + ) + with pytest.raises(RuntimeError, match="deliberate"): + with canonical_ni_processing(tree) as same: + assert same is tree + assert canonical.parent is canonical_parent + assert get_parameter(tree, PREFIX + "class_1.rates.employee.main") is legacy + assert not any( + isinstance(item, LegacyNIParameter) for item in tree.get_descendants() + ) + assert sum(item is canonical for item in tree.get_descendants()) == 1 + raise RuntimeError("deliberate failure") + assert legacy_parent.children is original_children + assert canonical.parent is canonical_parent + assert ( + build_parameters( + SimpleNamespace(tax_benefit_system=SimpleNamespace(parameters=tree)) + ) + == before + ) + + +def test_core_uprating_orders_legacy_dependencies_without_double_processing(): + from policyengine_core.parameters.operations.uprate_parameters import ( + uprate_parameters, + ) + from policyengine_uk.utils.ni_parameter_scales import ( + canonical_ni_processing, + canonical_ni_uprating_references, + ) + + tree = CountryTaxBenefitSystem().parameters.clone() + primary = tree.get_child(PREFIX + "class_1.thresholds.primary_threshold") + secondary = tree.get_child(PREFIX + "class_1.thresholds.secondary_threshold") + rate = tree.get_child(PREFIX + "class_1.rates.employee.main") + primary.update(start="2041-01-01", value=100) + primary.update(start="2042-01-01", value=200) + secondary.update(start="2041-01-01", value=50) + rate.update(start="2041-01-01", value=0.1) + secondary.metadata["uprating"] = PREFIX + "class_1.thresholds.primary_threshold" + rate.metadata["uprating"] = { + "parameter": PREFIX + "class_1.thresholds.secondary_threshold" + } + old_metadata = rate.metadata["uprating"] + with canonical_ni_processing(tree): + with canonical_ni_uprating_references(tree): + uprate_parameters(tree) + assert secondary("2042") == 100 + assert rate("2042") == 0.2 + assert rate.metadata["uprating"] is old_metadata + assert ( + tree.baseline.get_child(PREFIX + "class_1.rates.employee.main")("2042") == 0.08 + ) + + +def test_recursive_core_metadata_propagation_visits_each_authoritative_leaf_once(): + from policyengine_core.parameters.operations.propagate_parameter_metadata import ( + propagate_parameter_metadata, + ) + from policyengine_uk.utils.ni_parameter_scales import canonical_ni_processing + + tree = CountryTaxBenefitSystem().parameters.clone() + ni = tree.gov.hmrc.national_insurance + ni.metadata.update(propagate_metadata_to_children=True, scope_probe="owned-tree") + with canonical_ni_processing(tree): + propagate_parameter_metadata(tree) + for canonical in LEAVES.values(): + assert ( + tree.get_child(PREFIX + canonical).metadata["scope_probe"] == "owned-tree" + ) + assert ( + "scope_probe" + not in CountryTaxBenefitSystem().parameters.gov.hmrc.national_insurance.metadata + ) + + +@pytest.mark.parametrize( + "target", + ["legacy_leaf", "canonical_scale", "canonical_deleted", "canonical_leaf_deleted"], +) +def test_conflicting_structural_replacement_is_rejected_without_mutating_source(target): + from policyengine_core.parameters import ParameterNode, ParameterScale + + original = CountryTaxBenefitSystem() + changed = original.clone() + changed.reset_parameters() + ni = changed.parameters.gov.hmrc.national_insurance + if target == "legacy_leaf": + parent = ni.class_1.rates.employee + replacement = Parameter(parent.main.name, {"values": {"2026-01-01": 0.7}}) + parent.children["main"] = replacement + parent.main = replacement + replacement.parent = parent + elif target == "canonical_scale": + parent = ni.class_1 + replacement = ParameterScale( + parent.employee_scale.name, + { + "brackets": [ + {"threshold": {"2026-01-01": 0}, "rate": {"2026-01-01": 0.7}} + ] + }, + file_path=None, + ) + parent.children["employee_scale"] = replacement + parent.employee_scale = replacement + replacement.parent = parent + elif target == "canonical_deleted": + del ni.class_1.children["employee_scale"] + del ni.class_1.employee_scale + else: + bracket = ni.class_1.employee_scale.brackets[1] + del bracket.children["rate"] + del bracket.rate + with pytest.raises(ValueError, match="NI.*structurally replaced"): + changed.process_parameters() + assert ( + original.parameters.get_child(PREFIX + "class_1.rates.employee.main")("2026") + == 0.08 + ) + assert ( + original.parameters.get_child(PREFIX + "class_1.employee_scale[1].rate")("2026") + == 0.08 + ) + + +def test_supported_unrelated_structural_addition_survives_reset_and_processing(): + from policyengine_core.parameters import Parameter + + system = CountryTaxBenefitSystem() + leaf = Parameter("gov.test_222_extra", {"values": {"2026-01-01": 7}}) + system.parameters.gov.add_child("test_222_extra", leaf) + system.reset_parameters() + system.process_parameters() + assert system.parameters.gov.test_222_extra("2026") == 7 + assert ( + system.parameters.get_child(PREFIX + "class_1.rates.employee.main")("2026") + == 0.08 + ) + + +@pytest.mark.parametrize("reverse", [False, True]) +def test_mixed_alias_and_canonical_order_keeps_one_dated_timeline(reverse): + from policyengine_uk.utils.scenario import Scenario + from policyengine_uk import Simulation + + old = PREFIX + "class_1.rates.employee.main" + new = PREFIX + LEAVES["class_1.rates.employee.main"] + entries = [ + (old, {"2026-04-06.2027-04-05": 0.11}), + (new, {"2026-04-06.2027-04-05": 0.17}), + ] + if reverse: + entries.reverse() + sim = Simulation( + situation={ + "people": {"p": {"age": {2026: 40}}}, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + }, + scenario=Scenario.from_reform(dict(entries)), + ) + params = sim.tax_benefit_system.parameters + leaf = params.get_child(new) + assert leaf("2026") == (0.11 if reverse else 0.17) + assert leaf("2027") == 0.08 + assert params.get_child(old) is leaf + assert params.baseline.get_child(old)("2026") == 0.08 + assert all(a.path == new for a in sim.tax_benefit_system.dated_assignments) + + +@pytest.mark.parametrize( + "old, value", + [ + ("class_1.thresholds.primary_threshold", 300), + ("class_1.thresholds.upper_earnings_limit", 1100), + ("class_1.thresholds.secondary_threshold", 200), + ("class_4.thresholds.lower_profits_limit", 15000), + ("class_4.thresholds.upper_profits_limit", 60000), + ], +) +def test_all_five_generated_thresholds_keep_legacy_and_canonical_reform_routes( + old, value +): + from policyengine_uk.utils.scenario import Scenario + from policyengine_uk import Simulation + from policyengine_uk.parameters.gov.hmrc.create_threshold_indexation import ( + INDEXED_THRESHOLDS, + ) + + new = PREFIX + LEAVES[old] + assert new in INDEXED_THRESHOLDS + situations = { + "people": { + "p": { + "age": {2031: 40}, + "employment_income": {2031: 60000}, + "self_employment_income": {2031: 20000}, + } + }, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + } + a, b = [ + Simulation( + situation=situations, + scenario=Scenario.from_reform({path: {"2031-04-06.2032-04-05": value}}), + ) + for path in (PREFIX + old, new) + ] + for year in (2030, 2031, 2032, 2034): + left, right = a.tax_benefit_system.parameters, b.tax_benefit_system.parameters + assert left.get_child(new)(str(year)) == right.get_child(new)(str(year)) + assert left.get_child(PREFIX + old) is left.get_child(new) + assert a.tax_benefit_system.parameters.get_child(new)("2031") == value + import numpy as np + + np.testing.assert_array_equal( + a.calculate("national_insurance", 2031), b.calculate("national_insurance", 2031) + ) + np.testing.assert_array_equal( + a.calculate("household_net_income", 2031), + b.calculate("household_net_income", 2031), + ) + + +def test_raw_history_and_april_sampling_remain_distinct(): + system = CountryTaxBenefitSystem() + system.reset_parameters() + ni = system.parameters.gov.hmrc.national_insurance + assert ni.class_1.employee_scale.brackets[1].rate("2022-11-05") == 0.1325 + assert ni.class_1.employee_scale.brackets[1].rate("2022-11-06") == 0.12 + assert ni.class_1.rates.employee.main("2022-11-05") == 0.1325 + assert ni.class_1.rates.employee.main("2022-11-06") == 0.12 + system.process_parameters() + assert ( + system.parameters.gov.hmrc.national_insurance.class_1.employee_scale("2022") + .components[1] + .rate + == 0.1325 + ) + assert ( + system.parameters.gov.hmrc.national_insurance.class_1.rates.employee.main( + "2022" + ) + == 0.1325 + ) + + +def test_public_component_inputs_retain_override_and_aggregation_contract(): + from policyengine_uk import Simulation + from policyengine_uk.tests.test_ni_scale_equivalence import situation + + data = situation() + data["people"]["p0"]["ni_class_1_employee_primary"] = {2026: 123} + data["people"]["p0"]["ni_class_1_employee_additional"] = {2026: 45} + sim = Simulation(situation=data) + assert float(sim.calculate("ni_class_1_employee", 2026)[0]) == 168 + assert float(sim.calculate("national_insurance", 2026)[0]) == 168 + + +def test_legacy_parent_preserves_core_tracing_and_fresh_cache_reads(): + from types import SimpleNamespace + from policyengine_core.tracers import TracingParameterNodeAtInstant + + tree = CountryTaxBenefitSystem().parameters.clone() + parent = tree.gov.hmrc.national_insurance.class_1.rates.employee + reads = [] + parent.trace = True + parent.tracer = SimpleNamespace( + record_parameter_access=lambda *args: reads.append(args) + ) + parent.branch_name = "test_222" + first = parent("2026") + assert isinstance(first, TracingParameterNodeAtInstant) + assert first.main == 0.08 + tree.get_child(PREFIX + "class_1.employee_scale[1].rate").update( + period="2026", value=0.21 + ) + assert parent("2026").main == 0.21 + assert [row[0] for row in reads] == [PREFIX + "class_1.rates.employee.main"] * 2 + assert [row[-1] for row in reads] == [0.08, 0.21] + + +_NI_ZERO_SCALES = ( + "class_1.employee_scale", + "class_1.employer_scale", + "class_4.contribution_scale", +) +_NI_ZERO_LEAVES = tuple( + (scale, leaf) for scale in _NI_ZERO_SCALES for leaf in ("threshold", "rate") +) + + +def _ni_zero_error(scale, leaf): + import re + + path = PREFIX + scale + "[0]." + leaf + return r"NI.*" + re.escape(path) + r".*fixed at zero" + + +def _ni_zero_situation(): + return { + "people": { + "p": { + "age": {2026: 40}, + "employment_income": {2026: 60_000}, + "self_employment_income": {2026: 30_000}, + } + }, + "benunits": {"b": {"members": ["p"]}}, + "households": { + "h": { + "members": ["p"], + "region": {2026: "LONDON"}, + "tenure_type": {2026: "OWNED_OUTRIGHT"}, + "council_tax": {2026: 0}, + } + }, + } + + +def _ni_zero_scenario(route, changes): + from policyengine_core.reforms import Reform + from policyengine_uk.utils.scenario import Scenario + + if route == "changes": + return Scenario(parameter_changes=changes) + if route == "core": + return Scenario.from_reform(Reform.from_dict(changes)) + if route == "callable": + + class UpdateNILeaves(Reform): + def apply(self): + def update(parameters): + for path, values in changes.items(): + parameters.get_child(path).update( + period="2026", value=values["2026"] + ) + return parameters + + self.modify_parameters(update) + + return Scenario.from_reform(UpdateNILeaves) + return Scenario.from_reform(changes) + + +@pytest.mark.parametrize("scale,leaf", _NI_ZERO_LEAVES) +@pytest.mark.parametrize("route", ["dict", "changes", "core", "callable"]) +def test_ni_zero_band_reforms_are_rejected(scale, leaf, route): + from policyengine_uk import Simulation + + path = PREFIX + scale + "[0]." + leaf + changes = {path: {"2026": 0.5 if leaf == "rate" else 1}} + with pytest.raises(ValueError, match=_ni_zero_error(scale, leaf)): + sim = Simulation( + situation=_ni_zero_situation(), + scenario=_ni_zero_scenario(route, changes), + ) + sim.calculate("national_insurance", 2026) + + +@pytest.mark.parametrize("scale,leaf", _NI_ZERO_LEAVES) +@pytest.mark.parametrize("baseline", [False, True]) +def test_ni_zero_band_future_edits_cannot_hide_behind_a_cached_read( + scale, leaf, baseline +): + source = CountryTaxBenefitSystem().parameters + tree = source.clone() + target = tree.baseline if baseline else tree + target("2026") + path = PREFIX + scale + "[0]." + leaf + target.get_child(path).update(start="2041-01-01", value=1) + with pytest.raises(ValueError, match=_ni_zero_error(scale, leaf)): + target("2026") + assert source.get_child(path)("2041") == 0 + assert source.baseline.get_child(path)("2041") == 0 + + +@pytest.mark.parametrize("scale,leaf", _NI_ZERO_LEAVES) +@pytest.mark.parametrize("operation", ["process_parameters", "reset_parameter_caches"]) +def test_ni_zero_band_validation_rejects_updates_without_mutating_source( + scale, leaf, operation +): + source = CountryTaxBenefitSystem() + changed = source.clone() + if operation == "process_parameters": + changed.reset_parameters() + path = PREFIX + scale + "[0]." + leaf + changed.parameters.get_child(path).update(period="2026", value=1) + with pytest.raises(ValueError, match=_ni_zero_error(scale, leaf)): + getattr(changed, operation)() + assert source.parameters.get_child(path)("2026") == 0 + + +@pytest.mark.parametrize("scale,leaf", _NI_ZERO_LEAVES) +@pytest.mark.parametrize("damage", ["missing", "null"]) +def test_ni_zero_band_malformed_leaves_raise_the_targeted_error(scale, leaf, damage): + tree = CountryTaxBenefitSystem().parameters.clone() + raw_scale = tree.get_child(PREFIX + scale) + bracket = raw_scale.brackets[0] + if damage == "missing": + del bracket.children[leaf] + delattr(bracket, leaf) + else: + bracket.children[leaf].update(period="2026", value=None) + with pytest.raises(ValueError, match=_ni_zero_error(scale, leaf)): + raw_scale("2026") + + +@pytest.mark.parametrize("scale", _NI_ZERO_SCALES) +@pytest.mark.parametrize("route", ["dict", "changes", "core", "callable"]) +def test_ni_zero_band_explicit_zero_updates_are_noops(scale, route): + import numpy as np + from policyengine_uk import Simulation + + changes = { + PREFIX + scale + "[0]." + leaf: {"2026": 0} for leaf in ("threshold", "rate") + } + original = Simulation(situation=_ni_zero_situation()) + changed = Simulation( + situation=_ni_zero_situation(), + scenario=_ni_zero_scenario(route, changes), + ) + for output in ("national_insurance", "ni_class_1_employer", "household_net_income"): + np.testing.assert_array_equal( + changed.calculate(output, 2026), original.calculate(output, 2026) + ) diff --git a/policyengine_uk/tests/test_ni_scale_equivalence.py b/policyengine_uk/tests/test_ni_scale_equivalence.py new file mode 100644 index 0000000000..05c86695bc --- /dev/null +++ b/policyengine_uk/tests/test_ni_scale_equivalence.py @@ -0,0 +1,156 @@ +"""Differential against frozen original NI formulas, plus independent cash oracles. + +The original formula sources are from UK e1601dbe4feb15e71d0d59b7a57f9d860741c068. +They restore only those formulas on the same parameter/input tree, not an entire +old model or historical legal entitlement. Parameter histories and metadata have +separate exact pre-migration fixtures; malformed reforms are compatibility tests. +""" + +import numpy as np +import pytest +from policyengine_core.reforms import Reform +from policyengine_uk import Simulation +from policyengine_uk.utils.scenario import Scenario + +ORIGINAL_SOURCES = { + "policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_primary.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_1_employee_primary(Variable):\n label = "NI Class 1 employee-side primary contributions"\n entity = Person\n definition_period = MONTH\n value_type = float\n unit = GBP\n reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/8"\n\n def formula(person, period, parameters):\n income = person("ni_class_1_income", period)\n parameters = parameters(period).gov.hmrc.national_insurance.class_1\n\n # Thresholds are weekly, so multiply by weeks in year and divide by months in year\n primary_threshold = (\n parameters.thresholds.primary_threshold * WEEKS_IN_YEAR / MONTHS_IN_YEAR\n )\n upper_earnings_limit = (\n parameters.thresholds.upper_earnings_limit * WEEKS_IN_YEAR / MONTHS_IN_YEAR\n )\n\n upper_earnings_limit_income = max_(income - upper_earnings_limit, 0)\n primary_threshold_income = (\n max_(income - primary_threshold, 0) - upper_earnings_limit_income\n )\n\n return parameters.rates.employee.main * primary_threshold_income\n', + "policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employee_additional.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_1_employee_additional(Variable):\n label = "NI Class 1 employee-side additional contributions"\n entity = Person\n definition_period = MONTH\n value_type = float\n unit = GBP\n reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/8"\n\n def formula(person, period, parameters):\n income = person("ni_class_1_income", period)\n parameters = parameters(period).gov.hmrc.national_insurance.class_1\n upper_earnings_limit = (\n parameters.thresholds.upper_earnings_limit * WEEKS_IN_YEAR / MONTHS_IN_YEAR\n )\n upper_earnings_limit_income = max_(income - upper_earnings_limit, 0)\n return parameters.rates.employee.additional * upper_earnings_limit_income\n', + "policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_1_employer(Variable):\n value_type = float\n entity = Person\n label = "NI Class 1 employer-side contributions"\n definition_period = YEAR\n unit = GBP\n defined_for = "ni_class_1_secondary_liable"\n reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/9"\n\n def formula(person, period, parameters):\n class_1 = parameters(period).gov.hmrc.national_insurance.class_1\n earnings = person("ni_class_1_income", period)\n if not parameters(\n period\n ).gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions:\n added_pension_contributions = person(\n "employer_pension_contributions", period\n )\n taxed_earnings = earnings + added_pension_contributions\n else:\n taxed_earnings = earnings\n secondary_threshold = class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR\n main_earnings = max_(\n taxed_earnings - secondary_threshold,\n 0,\n )\n return class_1.rates.employer * main_earnings\n', + "policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_4(Variable):\n value_type = float\n entity = Person\n label = "NI Class 4 contributions"\n definition_period = YEAR\n unit = GBP\n defined_for = "ni_class_4_liable"\n reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/15"\n\n def formula(person, period, parameters):\n class_4 = parameters(period).gov.hmrc.national_insurance.class_4\n # Schedule 2: Class 4 is charged on the Chapter 2 trade profits less\n # the para 3 loss reliefs; Class 1 contributions are not a deduction.\n profits = person("ni_class_4_profits", period)\n add_rate_income = max_(\n profits - class_4.thresholds.upper_profits_limit,\n 0,\n )\n main_rate_income = (\n max_(\n profits - class_4.thresholds.lower_profits_limit,\n 0,\n )\n - add_rate_income\n )\n pre_maximum_amount = (\n main_rate_income * class_4.rates.main\n + add_rate_income * class_4.rates.additional\n )\n maximum_amount = person("ni_class_4_maximum", period)\n # Regulation 100(1): the annual maximum applies only where primary\n # Class 1 (or, before 6 April 2024, Class 2) contributions are also\n # payable for the year.\n includes_class_2 = class_4.annual_maximum.includes_class_2\n maximum_applies = (person("ni_class_1_employee", period) > 0) | np.logical_and(\n includes_class_2, person("ni_class_2", period) > 0\n )\n return max_(\n where(\n maximum_applies,\n min_(pre_maximum_amount, maximum_amount),\n pre_maximum_amount,\n ),\n 0,\n )\n', + "policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_4_main(Variable):\n value_type = float\n entity = Person\n label = "NI Class 4 main contributions"\n definition_period = YEAR\n unit = GBP\n defined_for = "ni_class_4_liable"\n reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/15"\n\n def formula(person, period, parameters):\n class_4 = parameters(period).gov.hmrc.national_insurance.class_4\n profits = person("ni_class_4_profits", period)\n add_rate_income = max_(\n profits - class_4.thresholds.upper_profits_limit,\n 0,\n )\n main_rate_income = (\n max_(\n profits - class_4.thresholds.lower_profits_limit,\n 0,\n )\n - add_rate_income\n )\n return main_rate_income * class_4.rates.main\n', + "policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py": 'from policyengine_uk.model_api import *\n\n\nclass ni_class_4_maximum(Variable):\n label = "NI Class 4 maximum liability"\n entity = Person\n definition_period = YEAR\n value_type = float\n unit = GBP\n reference = "https://www.legislation.gov.uk/uksi/2001/1004/regulation/100"\n\n def formula(person, period, parameters):\n ni = parameters(period).gov.hmrc.national_insurance\n upl = ni.class_4.thresholds.upper_profits_limit\n lpl = ni.class_4.thresholds.lower_profits_limit\n step_1 = upl - lpl\n # Regulation 100 writes its percentages out, but in every year from\n # 2015-16 they match the Class 4 rates: 9% and 2% (Steps Two, Five,\n # Eight and Nine) to 2021-22 and in 2023-24; 9.73% and 2.73% for\n # 2022-23 (Health and Social Care Levy (Repeal) Act 2022 Sch para\n # 6(2)); from 2024-25, 6% and 100/6 in Steps Two and Five (National\n # Insurance Contributions (Reduction in Rates) Act 2024 s 2(3)) with\n # Steps Eight and Nine still at 2%. So the steps read the rate\n # parameters.\n main_rate = ni.class_4.rates.main\n add_rate = ni.class_4.rates.additional\n step_2 = step_1 * main_rate\n # Class 2 contributions drop out of regulation 100 from 6 April 2024.\n includes_class_2 = ni.class_4.annual_maximum.includes_class_2\n class_2_weeks_addition = 53 * ni.class_2.flat_rate * includes_class_2\n step_3 = step_2 + class_2_weeks_addition\n class_2_contributions = person("ni_class_2", period) * includes_class_2\n primary_class_1_contributions = person("ni_class_1_employee_primary", period)\n step_4_raw = step_3 - class_2_contributions - primary_class_1_contributions\n step_4 = max_(step_4_raw, 0)\n # Case 1: Step Four exceeds primary Class 1 + Class 2 + main-rate\n # Class 4. Main-rate Class 4 is Step Two less the main rate on the\n # unused part of the main band, so this is equivalent to\n # main rate x unused band + 53 weekly Class 2 rates\n # > 2 x (primary Class 1 + Class 2).\n # Comparing the totals directly left the case to float32 rounding\n # when they were equal (profits above the UPL, no Class 1 or 2), and\n # a wrong Case 1 dropped the additional-rate band (#1878). The unused\n # band is exactly zero at or above the UPL, so that equality is now\n # decided exactly.\n profits = person("ni_class_4_profits", period)\n unused_main_band = clip(upl - profits.astype(np.float64), 0, step_1)\n case_1 = (step_4_raw >= 0) & (\n main_rate * unused_main_band + class_2_weeks_addition\n > 2\n * (\n class_2_contributions.astype(np.float64)\n + primary_class_1_contributions.astype(np.float64)\n )\n )\n # Step Five converts Step Four back to profits at the main rate. With a\n # zero main rate (a reform abolishing it) there is no main-rate\n # liability to limit, so the headroom is unlimited instead of 0 / 0.\n step_5 = np.divide(\n step_4,\n main_rate,\n out=np.full_like(step_4, np.inf),\n where=main_rate > 0,\n )\n step_6 = min_(upl, profits) - lpl\n step_7 = max_(0, step_6 - step_5)\n step_8 = step_7 * add_rate\n step_9 = max_(0, profits - upl) * add_rate\n\n return where(case_1, step_4, step_4 + step_8 + step_9)\n', +} + +ORIGINAL_CLASSES = [] +for source_path, source in ORIGINAL_SOURCES.items(): + namespace = {} + exec(compile(source, source_path, "exec"), namespace) + ORIGINAL_CLASSES.append(namespace[source_path.rsplit("/", 1)[-1][:-3]]) + + +class OriginalNIFormulas(Reform): + def apply(self): + for variable in ORIGINAL_CLASSES: + self.update_variable(variable) + + +def situation(): + facts = [(40, 30000, 0), (40, 60000, 18000), (70, 12000, 70000), (40, 0, 20000)] + people = { + f"p{i}": { + "age": {2026: age}, + "employment_income": {2026: wage}, + "self_employment_income": {2026: profit}, + } + for i, (age, wage, profit) in enumerate(facts) + } + return { + "people": people, + "benunits": {f"b{i}": {"members": [f"p{i}"]} for i in range(len(facts))}, + "households": { + f"h{i}": { + "members": [f"p{i}"], + "region": {2026: "LONDON"}, + "tenure_type": {2026: "OWNED_OUTRIGHT"}, + "council_tax": {2026: 0}, + } + for i in range(len(facts)) + }, + } + + +PREFIX = "gov.hmrc.national_insurance." +REFORMS = [ + {}, + {PREFIX + "class_1.rates.employee.main": {"2026": 0}}, + { + PREFIX + "class_1.thresholds.primary_threshold": {"2026": 200}, + PREFIX + "class_1.thresholds.upper_earnings_limit": {"2026": 200}, + }, + { + PREFIX + "class_1.thresholds.primary_threshold": {"2026": 300}, + PREFIX + "class_1.thresholds.upper_earnings_limit": {"2026": 200}, + }, + { + PREFIX + "class_4.thresholds.lower_profits_limit": {"2026": 20000}, + PREFIX + "class_4.thresholds.upper_profits_limit": {"2026": 10000}, + }, + { + "gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions": { + "2026": False + } + }, +] + + +@pytest.mark.parametrize("changes", REFORMS) +def test_component_and_final_cash_equal_frozen_original_formulas(changes): + current = Simulation(situation=situation(), scenario=Scenario.from_reform(changes)) + original = Simulation( + situation=situation(), + scenario=Scenario.from_reform((OriginalNIFormulas, changes)), + ) + outputs = [ + "ni_class_1_employee_primary", + "ni_class_1_employee_additional", + "ni_class_1_employee", + "ni_class_1_employer", + "ni_class_4_main", + "ni_class_4_maximum", + "ni_class_4", + "national_insurance", + "household_net_income", + ] + for name in outputs: + expected = original.calculate(name, 2026) + actual = current.calculate(name, 2026) + np.testing.assert_array_equal(actual, expected, err_msg=name) + assert actual.dtype == expected.dtype + # Query order cannot change a later output or the independent source system. + np.testing.assert_array_equal( + current.calculate("household_net_income", 2026), + original.calculate("household_net_income", 2026), + ) + + +def test_natural_2026_independent_ni_cash_examples(): + sim = Simulation(situation=situation()) + np.testing.assert_allclose( + sim.calculate("ni_class_1_employee", 2026), + [1394.4032, 3210.6008, 0, 0], + atol=0.002, + rtol=0, + ) + np.testing.assert_allclose( + sim.calculate("ni_class_1_employer", 2026), + [3750, 8250, 1050, 0], + atol=0.002, + rtol=0, + ) + assert float(sim.calculate("ni_class_4", 2026)[3]) == pytest.approx( + 445.8, abs=0.002 + ) + + +@pytest.mark.parametrize("route", ["dict", "core", "changes"]) +def test_scalar_and_canonical_leaf_reforms_have_same_natural_final_path(route): + old = {PREFIX + "class_1.rates.employee.main": {"2026": 0.1}} + new = {PREFIX + "class_1.employee_scale[1].rate": {"2026": 0.1}} + + def scenario(values): + if route == "changes": + return Scenario(parameter_changes=values) + if route == "core": + return Scenario.from_reform(Reform.from_dict(values)) + return Scenario.from_reform(values) + + a = Simulation(situation=situation(), scenario=scenario(old)) + b = Simulation(situation=situation(), scenario=scenario(new)) + np.testing.assert_array_equal( + a.calculate("ni_class_1_employee", 2026), + b.calculate("ni_class_1_employee", 2026), + ) + np.testing.assert_array_equal( + a.calculate("household_net_income", 2026), + b.calculate("household_net_income", 2026), + ) diff --git a/policyengine_uk/tests/test_ni_threshold_freeze.py b/policyengine_uk/tests/test_ni_threshold_freeze.py index 43cceb3712..f7fd758fa4 100644 --- a/policyengine_uk/tests/test_ni_threshold_freeze.py +++ b/policyengine_uk/tests/test_ni_threshold_freeze.py @@ -163,12 +163,26 @@ def test_no_threshold_falls_in_cash_terms(system, name): def test_frozen_year_values_cite_a_source(): """Every value dated in 2026-27 to 2030-31 carries a titled reference.""" for name, path in PATHS.items(): - file = ( - PARAMETER_DIR - / "gov/hmrc/national_insurance" - / (path.replace(".", "/") + ".yaml") - ) - values = yaml.safe_load(file.read_text())["values"] + component_paths = { + "primary_threshold": ("class_1/employee_scale.yaml", 1), + "upper_earnings_limit": ("class_1/employee_scale.yaml", 2), + "secondary_threshold": ("class_1/employer_scale.yaml", 1), + "lower_profits_limit": ("class_4/contribution_scale.yaml", 1), + "upper_profits_limit": ("class_4/contribution_scale.yaml", 2), + } + if name in component_paths: + relative, index = component_paths[name] + data = yaml.safe_load( + (PARAMETER_DIR / "gov/hmrc/national_insurance" / relative).read_text() + ) + values = data["brackets"][index]["threshold"]["values"] + else: + file = ( + PARAMETER_DIR + / "gov/hmrc/national_insurance" + / (path.replace(".", "/") + ".yaml") + ) + values = yaml.safe_load(file.read_text())["values"] dated = {str(instant): entry for instant, entry in values.items()} years = [2025, 2026] if name == "lower_earnings_limit" else FROZEN_YEARS for year in years: diff --git a/policyengine_uk/tests/test_threshold_indexation.py b/policyengine_uk/tests/test_threshold_indexation.py index 0e8961f917..828e3f10e2 100644 --- a/policyengine_uk/tests/test_threshold_indexation.py +++ b/policyengine_uk/tests/test_threshold_indexation.py @@ -96,11 +96,11 @@ PATHS = { "personal_allowance": "income_tax/allowances/personal_allowance/amount.yaml", "basic_rate_limit": "income_tax/rates/uk.yaml", - "primary_threshold": "national_insurance/class_1/thresholds/primary_threshold.yaml", - "upper_earnings_limit": "national_insurance/class_1/thresholds/upper_earnings_limit.yaml", - "secondary_threshold": "national_insurance/class_1/thresholds/secondary_threshold.yaml", - "lower_profits_limit": "national_insurance/class_4/thresholds/lower_profits_limit.yaml", - "upper_profits_limit": "national_insurance/class_4/thresholds/upper_profits_limit.yaml", + "primary_threshold": "national_insurance/class_1/employee_scale.yaml", + "upper_earnings_limit": "national_insurance/class_1/employee_scale.yaml", + "secondary_threshold": "national_insurance/class_1/employer_scale.yaml", + "lower_profits_limit": "national_insurance/class_4/contribution_scale.yaml", + "upper_profits_limit": "national_insurance/class_4/contribution_scale.yaml", } @@ -112,19 +112,19 @@ def system(): def threshold_parameters(root): """The seven parameters under a ``gov`` node.""" hmrc = root.hmrc - class_1 = hmrc.national_insurance.class_1.thresholds - class_4 = hmrc.national_insurance.class_4.thresholds + class_1 = hmrc.national_insurance.class_1 + class_4 = hmrc.national_insurance.class_4 return { "personal_allowance": hmrc.income_tax.allowances.personal_allowance.amount, # Bracket 1 itself: a scale read at an instant sorts its thresholds, # so its thresholds[1] would be the £125,140 additional rate threshold # once the basic rate limit passed it. "basic_rate_limit": hmrc.income_tax.rates.uk.brackets[1].threshold, - "primary_threshold": class_1.primary_threshold, - "upper_earnings_limit": class_1.upper_earnings_limit, - "secondary_threshold": class_1.secondary_threshold, - "lower_profits_limit": class_4.lower_profits_limit, - "upper_profits_limit": class_4.upper_profits_limit, + "primary_threshold": class_1.employee_scale.brackets[1].threshold, + "upper_earnings_limit": class_1.employee_scale.brackets[2].threshold, + "secondary_threshold": class_1.employer_scale.brackets[1].threshold, + "lower_profits_limit": class_4.contribution_scale.brackets[1].threshold, + "upper_profits_limit": class_4.contribution_scale.brackets[2].threshold, } @@ -432,19 +432,71 @@ def series(base, first, last, month_day="04-06"): }, "national_insurance": { "class_1": { - "thresholds": { - name: series(bases[name], 2021, last_years[name]) - for name in ( - "primary_threshold", - "upper_earnings_limit", - "secondary_threshold", - ) - } + "employee_scale": { + "brackets": [ + { + "threshold": {"values": {"2021-01-01": 0}}, + "rate": {"values": {"2021-01-01": 0}}, + }, + { + "threshold": series( + bases["primary_threshold"], + 2021, + last_years["primary_threshold"], + ), + "rate": {"values": {"2021-01-01": 0.08}}, + }, + { + "threshold": series( + bases["upper_earnings_limit"], + 2021, + last_years["upper_earnings_limit"], + ), + "rate": {"values": {"2021-01-01": 0.02}}, + }, + ] + }, + "employer_scale": { + "brackets": [ + { + "threshold": {"values": {"2021-01-01": 0}}, + "rate": {"values": {"2021-01-01": 0}}, + }, + { + "threshold": series( + bases["secondary_threshold"], + 2021, + last_years["secondary_threshold"], + ), + "rate": {"values": {"2021-01-01": 0.15}}, + }, + ] + }, }, "class_4": { - "thresholds": { - name: series(bases[name], 2021, last_years[name]) - for name in ("lower_profits_limit", "upper_profits_limit") + "contribution_scale": { + "brackets": [ + { + "threshold": {"values": {"2021-01-01": 0}}, + "rate": {"values": {"2021-01-01": 0}}, + }, + { + "threshold": series( + bases["lower_profits_limit"], + 2021, + last_years["lower_profits_limit"], + ), + "rate": {"values": {"2021-01-01": 0.06}}, + }, + { + "threshold": series( + bases["upper_profits_limit"], + 2021, + last_years["upper_profits_limit"], + ), + "rate": {"values": {"2021-01-01": 0.02}}, + }, + ] } }, }, @@ -455,28 +507,9 @@ def series(base, first, last, month_day="04-06"): def synthetic_values(root, year): - hmrc = root.gov.hmrc instant = f"{year}-04-30" return { - "personal_allowance": hmrc.income_tax.allowances.personal_allowance.amount( - instant - ), - "basic_rate_limit": hmrc.income_tax.rates.uk.brackets[1].threshold(instant), - "primary_threshold": hmrc.national_insurance.class_1.thresholds.primary_threshold( - instant - ), - "upper_earnings_limit": hmrc.national_insurance.class_1.thresholds.upper_earnings_limit( - instant - ), - "secondary_threshold": hmrc.national_insurance.class_1.thresholds.secondary_threshold( - instant - ), - "lower_profits_limit": hmrc.national_insurance.class_4.thresholds.lower_profits_limit( - instant - ), - "upper_profits_limit": hmrc.national_insurance.class_4.thresholds.upper_profits_limit( - instant - ), + name: leaf(instant) for name, leaf in threshold_parameters(root.gov).items() } @@ -707,6 +740,21 @@ def test_indexed_parameters_carry_no_uprating(name): data = yaml.safe_load((PARAMETER_DIR / "gov/hmrc" / PATHS[name]).read_text()) if name == "basic_rate_limit": metadata = data["brackets"][1]["threshold"]["metadata"] + elif name in [ + "primary_threshold", + "upper_earnings_limit", + "secondary_threshold", + "lower_profits_limit", + "upper_profits_limit", + ]: + index = { + "primary_threshold": 1, + "upper_earnings_limit": 2, + "secondary_threshold": 1, + "lower_profits_limit": 1, + "upper_profits_limit": 2, + }[name] + metadata = data["brackets"][index]["threshold"]["metadata"] else: metadata = data["metadata"] assert "uprating" not in metadata @@ -782,3 +830,23 @@ def test_income_tax_and_nics_use_the_indexed_thresholds_in_2031(system): assert primary[i] == pytest.approx(expected_primary, abs=0.02), income assert additional[i] == pytest.approx(expected_additional, abs=0.02), income assert np.all(np.diff(income_tax) >= 0) + + +def test_canonical_threshold_registration_keeps_incoming_savings_writer(): + """The merged indexation owns eight unique paths, including annual savings.""" + from policyengine_uk.parameters.gov.hmrc.create_threshold_indexation import ( + INDEXED_THRESHOLDS, + ) + + expected = { + "gov.hmrc.income_tax.allowances.personal_allowance.amount", + "gov.hmrc.income_tax.rates.uk[1].threshold", + "gov.hmrc.income_tax.rates.savings_starter_rate.allowance", + "gov.hmrc.national_insurance.class_1.employee_scale[1].threshold", + "gov.hmrc.national_insurance.class_1.employee_scale[2].threshold", + "gov.hmrc.national_insurance.class_1.employer_scale[1].threshold", + "gov.hmrc.national_insurance.class_4.contribution_scale[1].threshold", + "gov.hmrc.national_insurance.class_4.contribution_scale[2].threshold", + } + assert len(INDEXED_THRESHOLDS) == len(expected) + assert set(INDEXED_THRESHOLDS) == expected diff --git a/policyengine_uk/utils/dated_reforms.py b/policyengine_uk/utils/dated_reforms.py index 59751d91bb..53ef5f7796 100644 --- a/policyengine_uk/utils/dated_reforms.py +++ b/policyengine_uk/utils/dated_reforms.py @@ -185,6 +185,12 @@ last_stated_tax_year, ) +from policyengine_uk.utils.ni_parameter_scales import ( + LegacyNIParameter, + canonical_descendants, + canonicalize_ni_parameter_path, +) + from policyengine_uk.utils.parameters import ( BACKDATED_VALUE, check_parameter_not_removed, @@ -348,6 +354,7 @@ def read_reform_dict( legacy: List[LegacyOverride] = [] order = order_start for path, spec in reform.items(): + path = canonicalize_ni_parameter_path(path) _check_path(path) statutory = defines(path) if not isinstance(spec, Mapping): @@ -428,6 +435,7 @@ def read_parameter_changes( assignments: List[ParameterAssignment] = [] order = order_start for path, spec in changes.items(): + path = canonicalize_ni_parameter_path(path) _check_path(path) if not isinstance(spec, Mapping): start, stop = period_bounds(CORE_SCALAR_PERIOD) @@ -463,6 +471,7 @@ def read_reform_from_dict( legacy: List[LegacyOverride] = [] order = order_start for path, spec in parameter_values.items(): + path = canonicalize_ni_parameter_path(path) _check_path(path) if not defines(path): if isinstance(spec, Mapping): @@ -616,6 +625,7 @@ def read(spec): except (ValueError, TypeError, KeyError) as original: errors = [] for path, spec in changes.items(): + path = canonicalize_ni_parameter_path(path) entries = spec.items() if isinstance(spec, Mapping) else [(None, spec)] for key, value in entries: try: @@ -805,6 +815,7 @@ def read(spec, predicate, warn=False): return pending, bool(pending) overrides = [] for path, spec in changes.items(): + path = canonicalize_ni_parameter_path(path) change = {path: spec} assignments, legacy = read(change, defines) if _reform_writes_are_effective(assignments, legacy, system, written): @@ -986,7 +997,7 @@ def snapshot_timelines(node: ParameterNode) -> Dict[str, Timeline]: """ return { parameter.name: parameter.values_list - for parameter in node.get_descendants() + for parameter in canonical_descendants(node) if isinstance(parameter, Parameter) } @@ -1014,6 +1025,8 @@ def graft_missing_nodes( """ added = [] for key, child in source.children.items(): + if isinstance(child, LegacyNIParameter): + continue if root and key == "baseline": continue existing = target.children.get(key) diff --git a/policyengine_uk/utils/ni_parameter_scales.py b/policyengine_uk/utils/ni_parameter_scales.py new file mode 100644 index 0000000000..cecb696298 --- /dev/null +++ b/policyengine_uk/utils/ni_parameter_scales.py @@ -0,0 +1,377 @@ +"""Single-source NI scale leaves with legacy scalar read/update compatibility. + +Views are discoverable Parameters for existing metadata serializers. Numerical +processing explicitly excludes them; no view owns a second policy timeline. +Retire the Core-internal adaptations when stable alias/processing contracts +replace them. Remove legacy views through an explicit compatibility migration +once callers, saved reforms and metadata consumers use canonical scale paths; +retain their contract tests until that transition is complete. +""" + +import copy +from contextlib import contextmanager +from dataclasses import dataclass +from types import MappingProxyType + +from policyengine_core.commons import empty_clone +from policyengine_core.parameters import Parameter, ParameterNode, ParameterScale + +NI_PREFIX = "gov.hmrc.national_insurance." +NI_LEGACY_PATHS = MappingProxyType( + { + NI_PREFIX + old: NI_PREFIX + new + for old, new in { + "class_1.rates.employee.main": "class_1.employee_scale[1].rate", + "class_1.rates.employee.additional": "class_1.employee_scale[2].rate", + "class_1.thresholds.primary_threshold": "class_1.employee_scale[1].threshold", + "class_1.thresholds.upper_earnings_limit": "class_1.employee_scale[2].threshold", + "class_1.rates.employer": "class_1.employer_scale[1].rate", + "class_1.thresholds.secondary_threshold": "class_1.employer_scale[1].threshold", + "class_4.rates.main": "class_4.contribution_scale[1].rate", + "class_4.rates.additional": "class_4.contribution_scale[2].rate", + "class_4.thresholds.lower_profits_limit": "class_4.contribution_scale[1].threshold", + "class_4.thresholds.upper_profits_limit": "class_4.contribution_scale[2].threshold", + }.items() + } +) + + +def canonicalize_ni_parameter_path(path): + prefix = "" + while path.startswith("baseline."): + prefix += "baseline." + path = path[len("baseline.") :] + return prefix + NI_LEGACY_PATHS.get(path, path) + + +@dataclass(frozen=True) +class NIComponent: + """Original bracket identity, even when custom thresholds coincide/cross.""" + + threshold: object + rate: object + + +def _validate_ni_zero_bracket(scale): + """The initial zero band is a fixed representation sentinel.""" + bracket = scale.brackets[0] + for name in ("threshold", "rate"): + leaf = bracket.children.get(name) + if ( + not isinstance(leaf, Parameter) + or not leaf.values_list + or any(point.value != 0 for point in leaf.values_list) + ): + raise ValueError( + f"NI canonical leaf {scale.name}[0].{name} is fixed at zero; " + "update the contribution bands after bracket 0 instead" + ) + + +class NIParameterScale(ParameterScale): + def _get_at_instant(self, instant): + _validate_ni_zero_bracket(self) + scale = super()._get_at_instant(instant) + scale.components = tuple( + NIComponent(bracket.threshold(instant), bracket.rate(instant)) + for bracket in self.brackets + ) + return scale + + +class LegacyNIParameter(Parameter): + """A named scalar descriptor whose values belong to a canonical NI leaf.""" + + def __init__(self, name, target_path): + self.name = name + self.target_path = target_path + self.parent = None + self._at_instant_cache = {} + + @property + def target(self): + node = self.parent + while node is not None and not getattr(node, "_ni_scale_anchor", False): + node = getattr(node, "parent", None) + if node is None: + raise ValueError( + f"Detached NI compatibility view {self.name} has no policy tree" + ) + return node.get_child(self.target_path) + + @property + def values_list(self): + return self.target.values_list + + @values_list.setter + def values_list(self, value): + target = self.target + target.values_list = value + target.clear_parent_cache() + target.mark_as_modified() + + @property + def metadata(self): + return self.target.metadata + + @metadata.setter + def metadata(self, value): + self.target.metadata = value + + @property + def description(self): + return self.target.description + + @description.setter + def description(self, value): + self.target.description = value + + @property + def documentation(self): + return getattr(self.target, "documentation", None) + + @property + def file_path(self): + return self.target.file_path + + def update(self, *args, **kwargs): + return self.target.update(*args, **kwargs) + + def _get_at_instant(self, instant): + return self.target.get_at_instant(instant) + + def clear_parent_cache(self): + self.target.clear_parent_cache() + + def mark_as_modified(self): + self.target.mark_as_modified() + + def clone(self): + # A standalone leaf clone is an independent ordinary Parameter, as it + # was before migration. Parent cloning recreates descriptors instead. + leaf = self.target.clone() + leaf.name = self.name + owner = ParameterNode("", data={}) + owner.add_child("leaf", leaf) + return leaf + + +class LegacyNIDiscoveryParameter(LegacyNIParameter): + """Presentation-only alias; direct lookup keeps canonical metadata identity.""" + + @property + def metadata(self): + return {**self.target.metadata, "household": False, "economy": False} + + +class NICompatibilityNode(ParameterNode): + def get_descendants(self): + # Enumerators such as the API expose both stable public IDs, but only + # the canonical leaf is an editable discovery control. The registered + # child itself is unchanged for get_parameter/manual-child updates. + for child in self.children.values(): + if isinstance(child, LegacyNIParameter): + view = LegacyNIDiscoveryParameter(child.name, child.target_path) + view.parent = child.parent + yield view + else: + yield child + yield from child.get_descendants() + + def clone(self): + clone = empty_clone(self) + clone.__dict__ = self.__dict__.copy() + clone.metadata = copy.deepcopy(self.metadata) + clone.children = { + key: LegacyNIParameter(child.name, child.target_path) + if isinstance(child, LegacyNIParameter) + else child.clone() + for key, child in self.children.items() + } + for key, child in clone.children.items(): + setattr(clone, key, child) + child.parent = clone + clone._at_instant_cache = {} + return clone + + def _get_at_instant(self, instant): + # A sibling canonical update clears the root's cache, not a legacy + # sibling's cache. Rebuild this cheap numeric view on every lookup. + self._at_instant_cache.pop(instant, None) + return super()._get_at_instant(instant) + + +class _CanonicalChildren(dict): + """Explicit processing iteration, retaining legacy key lookups.""" + + def items(self): + return ( + (key, value) + for key, value in super().items() + if not isinstance(value, LegacyNIParameter) + ) + + def values(self): + return (value for _, value in self.items()) + + def __iter__(self): + return (key for key, _ in self.items()) + + +def canonical_descendants(node): + return ( + child + for child in node.get_descendants() + if not isinstance(child, LegacyNIParameter) + ) + + +@contextmanager +def canonical_ni_processing(parameters): + """Only on an owned processing tree; restore discovery even on errors. + + The real root/leaves and their parents retain identity. Core's nested + traversal and its independent child/path walker see canonical leaves; + explicit legacy lookup still resolves the view. No global/caller mode. + """ + saved = [] + try: + for node in list(parameters.get_descendants()): + if isinstance(node, ParameterNode) and any( + isinstance(child, LegacyNIParameter) for child in node.children.values() + ): + saved.append((node, node.children)) + node.children = _CanonicalChildren(node.children) + yield parameters + finally: + for node, children in reversed(saved): + node.children = children + + +@contextmanager +def canonical_ni_uprating_references(parameters): + """Map NI dependency names for Core's canonical-leaf ordering only.""" + saved = [] + try: + for leaf in canonical_descendants(parameters): + if not isinstance(leaf, Parameter): + continue + value = leaf.metadata.get("uprating") + path = ( + value + if isinstance(value, str) + else (value.get("parameter") if isinstance(value, dict) else None) + ) + if path is None: + continue + canonical = canonicalize_ni_parameter_path(path) + if canonical != path: + saved.append((leaf.metadata, value)) + leaf.metadata["uprating"] = ( + canonical + if isinstance(value, str) + else { + **value, + "parameter": canonical, + } + ) + yield parameters + finally: + for metadata, value in reversed(saved): + metadata["uprating"] = value + + +def add_ni_scale_compatibility(parameters): + """Install relative, clone-safe views without copied scalar histories.""" + # Baseline copies use their own NI anchor and canonical components. + roots = [parameters] + if "baseline" in parameters.children: + roots.append(parameters.children["baseline"]) + for root in roots: + ni = root.gov.hmrc.national_insurance + for name in ( + "class_1.employee_scale", + "class_1.employer_scale", + "class_4.contribution_scale", + ): + scale = ni.get_child(name) + scale.__class__ = NIParameterScale + ni._ni_scale_anchor = True + for old, new in NI_LEGACY_PATHS.items(): + parts = old[len(NI_PREFIX) :].split(".") + node = ni + for part in parts[:-1]: + node.__class__ = NICompatibilityNode + if part not in node.children: + node.add_child( + part, NICompatibilityNode(node.name + "." + part, data={}) + ) + node = node.children[part] + if not isinstance(node, ParameterNode): + raise ValueError( + f"NI legacy subtree {old} was structurally replaced; use {new}" + ) + node.__class__ = NICompatibilityNode + previous = node.children.get(parts[-1]) + if previous is not None and not isinstance(previous, LegacyNIParameter): + raise ValueError( + f"NI legacy leaf {old} was structurally replaced; update {new}" + ) + if previous is None: + node.add_child(parts[-1], LegacyNIParameter(old, new[len(NI_PREFIX) :])) + return parameters + + +def validate_ni_scale_compatibility(parameters): + """Reject conflicting subtree replacement before numerical processing. + + Leaf updates and unrelated added nodes are supported. Replacing either + spelling's structure is not silently interpreted as a second schedule. + """ + roots = [parameters] + if "baseline" in parameters.children: + roots.append(parameters.children["baseline"]) + for root in roots: + try: + ni = root.gov.hmrc.national_insurance + except (AttributeError, ValueError) as error: + raise ValueError( + "NI canonical subtree was structurally replaced; retain the schedules and update component leaves" + ) from error + for path, count in ( + ("class_1.employee_scale", 3), + ("class_1.employer_scale", 2), + ("class_4.contribution_scale", 3), + ): + try: + scale = ni.get_child(path) + except (KeyError, AttributeError, ValueError) as error: + raise ValueError( + f"NI canonical schedule {path} was structurally replaced; update its component leaves" + ) from error + if not isinstance(scale, NIParameterScale) or len(scale.brackets) != count: + raise ValueError( + f"NI canonical schedule {path} was structurally replaced; update its component leaves" + ) + _validate_ni_zero_bracket(scale) + for old, new in NI_LEGACY_PATHS.items(): + node = ni + try: + for part in old[len(NI_PREFIX) :].split("."): + node = node.children[part] + except (KeyError, AttributeError, ValueError) as error: + raise ValueError( + f"NI legacy path {old} was structurally replaced; update {new}" + ) from error + try: + valid = isinstance( + node, LegacyNIParameter + ) and node.target is root.get_child(new) + except (KeyError, AttributeError, ValueError) as error: + raise ValueError( + f"NI canonical leaf {new} was structurally replaced; update its value" + ) from error + if not valid: + raise ValueError( + f"NI legacy path {old} was structurally replaced; update {new}" + ) diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index 66f93aa66a..3cee219759 100644 --- a/policyengine_uk/utils/parameters.py +++ b/policyengine_uk/utils/parameters.py @@ -4,6 +4,12 @@ from policyengine_core.model_api import * from policyengine_core import periods +from policyengine_uk.utils.ni_parameter_scales import ( + add_ni_scale_compatibility, + canonicalize_ni_parameter_path, + canonical_descendants, +) + from policyengine_uk.parameters.gov.simulation.labour_supply_responses.aliases import ( canonicalize_lsr_parameter_path, ) @@ -25,7 +31,7 @@ def str_to_instant(s): def backdate_parameters(root: str = None, first_instant: str = "2021-01-01") -> Reform: first_instant = str_to_instant(first_instant) node = root - for param in node.get_descendants(): + for param in canonical_descendants(node): if isinstance(param, Parameter): earliest = param.values_list[-1] earliest_value = earliest.value @@ -117,7 +123,7 @@ def convert_to_fiscal_year_parameters(parameters): """ # Cover years from 2015 through 2040 for long-term projections YEARS = list(range(2015, 2041)) - for param in parameters.get_descendants(): + for param in canonical_descendants(parameters): if isinstance(param, Parameter): if (param.metadata or {}).get("preserve_calendar_dates", False): continue @@ -225,7 +231,7 @@ class RemovedParameterRoot(ParameterNode): def get_child(self, path: str): check_parameter_not_removed(path) - return super().get_child(path) + return super().get_child(canonicalize_ni_parameter_path(path)) def add_removed_parameter_aliases(parameters: ParameterNode) -> ParameterNode: @@ -265,4 +271,4 @@ def add_removed_parameter_aliases(parameters: ParameterNode) -> ParameterNode: parent.children[name] = alias setattr(parent, name, alias) alias.parent = parent - return parameters + return add_ni_scale_compatibility(parameters) diff --git a/policyengine_uk/variables/contrib/policyengine/employer_ni/adjusted_employer_cost.py b/policyengine_uk/variables/contrib/policyengine/employer_ni/adjusted_employer_cost.py index 863a35adfa..d4e2d50547 100644 --- a/policyengine_uk/variables/contrib/policyengine/employer_ni/adjusted_employer_cost.py +++ b/policyengine_uk/variables/contrib/policyengine/employer_ni/adjusted_employer_cost.py @@ -29,8 +29,8 @@ def formula(person, period, parameters): # Calculate employer cost parameters = parameters(period) class_1 = parameters.gov.hmrc.national_insurance.class_1 - r_r = class_1.rates.employer - t_r = class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR + r_r = class_1.employer_scale.components[1].rate + t_r = class_1.employer_scale.components[1].threshold * WEEKS_IN_YEAR p_r = parameters.gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions pen_con_subtracted_r = employer_pension_contributions if p_r: diff --git a/policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py b/policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py index 3f32b7291e..0a929b7530 100644 --- a/policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py +++ b/policyengine_uk/variables/contrib/policyengine/employer_ni/baseline_employer_cost.py @@ -39,8 +39,8 @@ def formula(person, period, parameters): period ) baseline_class_1 = baseline_parameters.gov.hmrc.national_insurance.class_1 - r_b = baseline_class_1.rates.employer - t_b = baseline_class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR + r_b = baseline_class_1.employer_scale.components[1].rate + t_b = baseline_class_1.employer_scale.components[1].threshold * WEEKS_IN_YEAR p_b = baseline_parameters.gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions pen_con_subtracted_b = employer_pension_contributions if p_b: diff --git a/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py b/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py index be4aa17063..37af8cc502 100644 --- a/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py +++ b/policyengine_uk/variables/contrib/policyengine/employer_ni/employer_ni_fixed_employer_cost_change.py @@ -47,8 +47,8 @@ def formula(person, period, parameters): ) baseline_class_1 = baseline_parameters.gov.hmrc.national_insurance.class_1 - r_b = baseline_class_1.rates.employer - t_b = baseline_class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR + r_b = baseline_class_1.employer_scale.components[1].rate + t_b = baseline_class_1.employer_scale.components[1].threshold * WEEKS_IN_YEAR p_b = baseline_parameters.gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions pen_con_subtracted_b = employer_pension_contributions if p_b else 0 @@ -60,8 +60,8 @@ def formula(person, period, parameters): # Calculate reform employer cost reform_parameters = parameters(period) reform_class_1 = reform_parameters.gov.hmrc.national_insurance.class_1 - r_r = reform_class_1.rates.employer - t_r = reform_class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR + r_r = reform_class_1.employer_scale.components[1].rate + t_r = reform_class_1.employer_scale.components[1].threshold * WEEKS_IN_YEAR p_r = reform_parameters.gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions pen_con_subtracted_r = employer_pension_contributions if p_r else 0 diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py index e3dc980d29..f8f0778826 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -102,11 +102,11 @@ def formula(person, period, parameters): trading_tax = basic_rate * max_(0, profit - personal_allowance) trading_class_4 = ( person("ni_class_4_liable", period) - * class_4.rates.main + * class_4.contribution_scale.components[1].rate * max_( 0, - min_(profit, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, + min_(profit, class_4.contribution_scale.components[2].threshold) + - class_4.contribution_scale.components[1].threshold, ) ) # Reg. 14(2)(a), omitted from 6 April 2024: the Class 2 contributions diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 4f82cef51c..eeb7ab10a2 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -100,13 +100,17 @@ def formula(person, period, parameters): tax_on_pay = max_(0, tax_on_pay - mca_reduction) nics = p.gov.hmrc.national_insurance class_1 = nics.class_1 - primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR - upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR + primary_threshold = ( + class_1.employee_scale.components[1].threshold * WEEKS_IN_YEAR + ) + upper_earnings_limit = ( + class_1.employee_scale.components[2].threshold * WEEKS_IN_YEAR + ) pay_above_upper_limit = max_(0, pay - upper_earnings_limit) class_1_on_pay = person("ni_liable", period) * ( - class_1.rates.employee.main + class_1.employee_scale.components[1].rate * (max_(0, pay - primary_threshold) - pay_above_upper_limit) - + class_1.rates.employee.additional * pay_above_upper_limit + + class_1.employee_scale.components[2].rate * pay_above_upper_limit ) pension_from_pay = ( employee_pension + personal_pension - personal_pension_from_profit @@ -135,11 +139,13 @@ def formula(person, period, parameters): class_4 = nics.class_4 class_4_band = max_( 0, - min_(profit, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, + min_(profit, class_4.contribution_scale.components[2].threshold) + - class_4.contribution_scale.components[1].threshold, ) notional_class_4 = ( - person("ni_class_4_liable", period) * class_4.rates.main * class_4_band + person("ni_class_4_liable", period) + * class_4.contribution_scale.components[1].rate + * class_4_band ) # Reg 99(3)(a), until 5 April 2024: Class 2 at the weekly rate unless # the profit is below the small profits threshold or, from 6 April diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py index d8be62b50a..8ac7b852c4 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_self_employment_deductions.py @@ -56,11 +56,12 @@ def formula(person, period, parameters): - person("married_couples_allowance_deduction", period), ) class_4 = parameters(period).gov.hmrc.national_insurance.class_4 - lower = class_4.thresholds.lower_profits_limit - upper = class_4.thresholds.upper_profits_limit + lower = class_4.contribution_scale.components[1].threshold + upper = class_4.contribution_scale.components[2].threshold notional_class_4 = where( person("ni_class_4_liable", period), - class_4.rates.main * min_(max_(0, profit - lower), upper - lower), + class_4.contribution_scale.components[1].rate + * min_(max_(0, profit - lower), upper - lower), 0, ) class_2 = parameters(period).gov.hmrc.national_insurance.class_2 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py index 19025089f2..4d601ff00c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py @@ -35,11 +35,15 @@ def formula(person, period, parameters): # Primary Class 1 on pay equal to the threshold, with the annual # thresholds (weekly amounts times 52) the model uses for employees. class_1 = ni.class_1 - primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR - upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR - employee = class_1.rates.employee.main * max_( + primary_threshold = ( + class_1.employee_scale.components[1].threshold * WEEKS_IN_YEAR + ) + upper_earnings_limit = ( + class_1.employee_scale.components[2].threshold * WEEKS_IN_YEAR + ) + employee = class_1.employee_scale.components[1].rate * max_( 0, min_(threshold, upper_earnings_limit) - primary_threshold - ) + class_1.rates.employee.additional * max_( + ) + class_1.employee_scale.components[2].rate * max_( 0, threshold - upper_earnings_limit ) # Class 2 and Class 4 on profits equal to the threshold, by the same @@ -51,12 +55,12 @@ def formula(person, period, parameters): * class_2_contribution_weeks(period.start.year) ) class_4 = ni.class_4 - class_4_amount = class_4.rates.main * max_( + class_4_amount = class_4.contribution_scale.components[1].rate * max_( 0, - min_(threshold, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, - ) + class_4.rates.additional * max_( - 0, threshold - class_4.thresholds.upper_profits_limit + min_(threshold, class_4.contribution_scale.components[2].threshold) + - class_4.contribution_scale.components[1].threshold, + ) + class_4.contribution_scale.components[2].rate * max_( + 0, threshold - class_4.contribution_scale.components[2].threshold ) # Each class is due only from those the model treats as liable for # it, as for the person's actual contributions. Primary Class 1 and 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 ef7b99db7f..76d002631b 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 @@ -13,10 +13,14 @@ def formula(person, period, parameters): income = person("ni_class_1_income", period) parameters = parameters(period).gov.hmrc.national_insurance.class_1 upper_earnings_limit = ( - parameters.thresholds.upper_earnings_limit * WEEKS_IN_YEAR / MONTHS_IN_YEAR + parameters.employee_scale.components[2].threshold + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR ) upper_earnings_limit_income = max_(income - upper_earnings_limit, 0) - ordinary = parameters.rates.employee.additional * upper_earnings_limit_income + ordinary = ( + parameters.employee_scale.components[2].rate * upper_earnings_limit_income + ) directors = parameters.directors.full_year_category_a_2022_23 if not directors.in_effect: return 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 f29602fa40..8464b3db3f 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 @@ -15,10 +15,14 @@ def formula(person, period, parameters): # Thresholds are weekly, so multiply by weeks in year and divide by months in year primary_threshold = ( - parameters.thresholds.primary_threshold * WEEKS_IN_YEAR / MONTHS_IN_YEAR + parameters.employee_scale.components[1].threshold + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR ) upper_earnings_limit = ( - parameters.thresholds.upper_earnings_limit * WEEKS_IN_YEAR / MONTHS_IN_YEAR + parameters.employee_scale.components[2].threshold + * WEEKS_IN_YEAR + / MONTHS_IN_YEAR ) upper_earnings_limit_income = max_(income - upper_earnings_limit, 0) @@ -26,7 +30,9 @@ def formula(person, period, parameters): max_(income - primary_threshold, 0) - upper_earnings_limit_income ) - ordinary = parameters.rates.employee.main * primary_threshold_income + ordinary = ( + parameters.employee_scale.components[1].rate * primary_threshold_income + ) directors = parameters.directors.full_year_category_a_2022_23 if not directors.in_effect: return ordinary diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py index a179befa83..c4ad894ea7 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py @@ -22,9 +22,11 @@ def formula(person, period, parameters): taxed_earnings = earnings + added_pension_contributions else: taxed_earnings = earnings - secondary_threshold = class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR + secondary_threshold = ( + class_1.employer_scale.components[1].threshold * WEEKS_IN_YEAR + ) main_earnings = max_( taxed_earnings - secondary_threshold, 0, ) - return class_1.rates.employer * main_earnings + return class_1.employer_scale.components[1].rate * main_earnings diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py index 0657f27c97..eba6649452 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py @@ -16,19 +16,19 @@ def formula(person, period, parameters): # the para 3 loss reliefs; Class 1 contributions are not a deduction. profits = person("ni_class_4_profits", period) add_rate_income = max_( - profits - class_4.thresholds.upper_profits_limit, + profits - class_4.contribution_scale.components[2].threshold, 0, ) main_rate_income = ( max_( - profits - class_4.thresholds.lower_profits_limit, + profits - class_4.contribution_scale.components[1].threshold, 0, ) - add_rate_income ) pre_maximum_amount = ( - main_rate_income * class_4.rates.main - + add_rate_income * class_4.rates.additional + main_rate_income * class_4.contribution_scale.components[1].rate + + add_rate_income * class_4.contribution_scale.components[2].rate ) maximum_amount = person("ni_class_4_maximum", period) # Regulation 100(1): the annual maximum applies only where primary diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py index 8ba64c53b3..98c4a91e0d 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py @@ -14,14 +14,14 @@ def formula(person, period, parameters): class_4 = parameters(period).gov.hmrc.national_insurance.class_4 profits = person("ni_class_4_profits", period) add_rate_income = max_( - profits - class_4.thresholds.upper_profits_limit, + profits - class_4.contribution_scale.components[2].threshold, 0, ) main_rate_income = ( max_( - profits - class_4.thresholds.lower_profits_limit, + profits - class_4.contribution_scale.components[1].threshold, 0, ) - add_rate_income ) - return main_rate_income * class_4.rates.main + return main_rate_income * class_4.contribution_scale.components[1].rate diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py index f599002047..69128f473b 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.py @@ -11,8 +11,8 @@ class ni_class_4_maximum(Variable): def formula(person, period, parameters): ni = parameters(period).gov.hmrc.national_insurance - upl = ni.class_4.thresholds.upper_profits_limit - lpl = ni.class_4.thresholds.lower_profits_limit + upl = ni.class_4.contribution_scale.components[2].threshold + lpl = ni.class_4.contribution_scale.components[1].threshold step_1 = upl - lpl # Regulation 100 writes its percentages out, but in every year from # 2015-16 they match the Class 4 rates: 9% and 2% (Steps Two, Five, @@ -22,8 +22,8 @@ def formula(person, period, parameters): # Insurance Contributions (Reduction in Rates) Act 2024 s 2(3)) with # Steps Eight and Nine still at 2%. So the steps read the rate # parameters. - main_rate = ni.class_4.rates.main - add_rate = ni.class_4.rates.additional + main_rate = ni.class_4.contribution_scale.components[1].rate + add_rate = ni.class_4.contribution_scale.components[2].rate step_2 = step_1 * main_rate # Class 2 contributions drop out of regulation 100 from 6 April 2024. includes_class_2 = ni.class_4.annual_maximum.includes_class_2 diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py b/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py index d9a8cce55d..661c99bce2 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.py @@ -23,14 +23,16 @@ def formula(person, period, parameters): # Use existing NI Class 1 parameters ni_params = parameters(period).gov.hmrc.national_insurance.class_1 employment_income = person("employment_income", period) - upper_earnings_limit = ni_params.thresholds.upper_earnings_limit * WEEKS_IN_YEAR + upper_earnings_limit = ( + ni_params.employee_scale.components[2].threshold * WEEKS_IN_YEAR + ) # Apply appropriate NI rate based on income level # Main rate (8%) for income <= UEL, additional rate (2%) for income > UEL ni_rate = where( employment_income <= upper_earnings_limit, - ni_params.rates.employee.main, - ni_params.rates.employee.additional, + ni_params.employee_scale.components[1].rate, + ni_params.employee_scale.components[2].rate, ) return excess * ni_rate diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py b/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py index c20736b37c..43593e344e 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.py @@ -22,6 +22,6 @@ def formula(person, period, parameters): # Use existing NI Class 1 employer rate parameter ni_params = parameters(period).gov.hmrc.national_insurance.class_1 - employer_rate = ni_params.rates.employer + employer_rate = ni_params.employer_scale.components[1].rate return excess * employer_rate