Skip to content

Commit

Permalink
Import and refactor code from SavingTool source
Browse files Browse the repository at this point in the history
The main changes are support for multiple tax years and some more consistent and concise naming. Also changed the NICs function to accept annual taxable income instead of weekly
  • Loading branch information
sgb-io committed May 26, 2022
1 parent ff8c59a commit 90fa312
Show file tree
Hide file tree
Showing 13 changed files with 909 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "eslint:recommended"
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
yarn-error.log
lib
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
79 changes: 77 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,80 @@
# HMRC Income Tax

The idea is to extract the income tax and NICs calculations from https://github.com/sgb-io/saving-tool/tree/main/src/common and make it open source.
A TypeScript implementation of UK Income Tax & National Insurance calculations. See it in action on [SavingTool.co.uk](https://savingtool.co.uk).

It would be good to add support for multiple tax years (esp since new numbers are likely to be announced as April approaches)
Multiple versions of the HMRC rates can be supported, although only 2022/23 has been implemented (see `src/hmrc.ts`).

## Installation

`yarn add @saving-tool/hmrc-income-tax` (or `npm install @saving-tool/hmrc-income-tax`)

## Usage

There are 4 functions exposed:

- `calculatePersonalAllowance`: calculates an individual's personal allowance for a tax year, single amount.
- `calculateIncomeTax`: calculates the income tax due in a tax year on an individual's taxable income, broken down into the 3 bands (basic, higher, additional)
- `calculateEmployeeNationalInsurance`: calculates the national insurance contributions due in a tax year on an individual's taxable income, single amount. Note: only supports class 1, category A
- `calculateStudentLoanRepayments`: calculates the student loan repayments due in a tax year on an individual's taxable income, single amount.

All APIs return raw amounts and there is no formatting or display functionality.

## Examples (2022/23 HMRC Rates)

Mark S. of MDR earns £55,000. His employer contributes 6% to his pension, but also matches up to another 2%. Mark contributes 2% via salary sacrafice to get the matching. Therefore, Mark's taxable income is £53,900. He has £19,000 of outstanding student loan debt, and is on Plan 1.

```javascript
// Mark S.
const taxableAnnualIncome = 53_900
const personalAllowance = calculatePersonalAllowance({ taxableAnnualIncome }); // => 12570
const incomeTax = calculateIncomeTax({ personalAllowance, taxableAnnualIncome });
const { basicRateTax, higherRateTax, additionalRateTax } = incomeTax;
const totalIncomeTax = basicRateTax + higherRateTax + additionalRateTax; // => 8992
const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ taxableAnnualIncome }); // => 5471
const studentLoanRepayments = calculateStudentLoanRepayments({ taxableAnnualIncome, studentLoanPlanNo: 1 }); // => 3162

// Do whatever you want, e.g. calculate the take-home pay
const takeHome = taxableAnnualIncome - totalIncomeTax - nationalInsuranceContributions - studentLoanRepayments; // => 36275
```


Irv B. of MDR earns £160,000. His employer contributes some amount to his pension, but he contributes nothing. He has no student loan.

```javascript
// Irv B.
const taxableAnnualIncome = 160_000
const personalAllowance = calculatePersonalAllowance({ taxableAnnualIncome }); // => 0
const incomeTax = calculateIncomeTax({ personalAllowance, taxableAnnualIncome });
const { basicRateTax, higherRateTax, additionalRateTax } = incomeTax;
const totalIncomeTax = basicRateTax + higherRateTax + additionalRateTax; // => 57589
const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ taxableAnnualIncome }); // => 8919

// Do whatever you want, e.g. calculate the take-home pay
const takeHome = taxableAnnualIncome - totalIncomeTax - nationalInsuranceContributions; // => 93492
```


It's important to understand that in most cases this library is expecting *taxable* income (appropriate API naming aims to make this clear). Any salary sacrafice mechanisms should be applied before these calculations, and the appropriate taxable amount used when calling this library.


## Formatting and rounding output

A formatter/rounder function is not included as to separate that concern from the raw tax calculations. Your application may want to apply it's own rounding and formatting logic.

Example roll-your-own formatter using the `Intl` API (similar to what [SavingTool.co.uk](https://savingtool.co.uk) uses):

```javascript
const gbpFormatter = new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});

// Rounds an amount of GBP to the nearest pound and formats it
// `amount` can be a long number e.g. 548.729345847 => £549
export const roundAndFormatGbp = (amount: number) => {
return formatGbp(Math.round(amount));
};

```
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"devDependencies": {
"eslint": "^8.16.0",
"prettier": "^2.6.2",
"typescript": "^4.7.2"
}
}
57 changes: 57 additions & 0 deletions src/hmrc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { TaxYear } from "./types";

interface TaxRates {
// Income Tax
// See https://www.gov.uk/income-tax-rates
DEFAULT_PERSONAL_ALLOWANCE: number;
HIGHER_BRACKET: number;
ADDITIONAL_BRACKET: number;
BASIC_RATE: number;
HIGHER_RATE: number;
ADDITIONAL_RATE: number;
PERSONAL_ALLOWANCE_DROPOFF: number;

// Student loan repayments
// See https://www.gov.uk/repaying-your-student-loan/what-you-pay
STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD: number;
STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD: number;
// People on plans 1 or 2 repay 9% of the amount you earn over the threshold
STUDENT_LOAN_REPAYMENT_AMOUNT: number;

// National Insurance
// See https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023 for current and previous rates
NI_MIDDLE_RATE: number;
NI_UPPER_RATE: number;
NI_MIDDLE_BRACKET: number;
NI_UPPER_BRACKET: number;
}

const taxRates: Record<TaxYear, TaxRates> = {
"2022/23": {
// Income tax
DEFAULT_PERSONAL_ALLOWANCE: 12_570,
HIGHER_BRACKET: 50_270,
ADDITIONAL_BRACKET: 150_000,
BASIC_RATE: 0.2,
HIGHER_RATE: 0.4,
ADDITIONAL_RATE: 0.45,
PERSONAL_ALLOWANCE_DROPOFF: 100_000,
// Student loan repayments
STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD: 382,
STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD: 524,
STUDENT_LOAN_REPAYMENT_AMOUNT: 0.09,
// National Insurance
NI_MIDDLE_RATE: 0.1325,
NI_UPPER_RATE: 0.0325,
NI_MIDDLE_BRACKET: 190,
NI_UPPER_BRACKET: 967,
},
};

export const getHmrcRates = (taxYear?: TaxYear) => {
if (!taxYear) {
return taxRates["2022/23"];
}

return taxRates[taxYear];
};
80 changes: 80 additions & 0 deletions src/incomeTax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { IncomeTax, TaxYear } from "./types";
import { getHmrcRates } from "./hmrc";

// Calculates an indivudals income tax against annual taxable income
// Note: National Insurance contributions are not included here, see `calculateEmployeeNationalInsurance` instead
export const calculateIncomeTax = ({
taxYear,
taxableAnnualIncome,
personalAllowance,
}: {
taxYear?: TaxYear;
taxableAnnualIncome: number; // Pre-tax income (before any taxes or NI contributions)
personalAllowance: number; // The individual's personal allowance
}): IncomeTax => {
const {
ADDITIONAL_BRACKET,
ADDITIONAL_RATE,
BASIC_RATE,
DEFAULT_PERSONAL_ALLOWANCE,
HIGHER_BRACKET,
HIGHER_RATE,
} = getHmrcRates(taxYear);
const afterPersonalAllowance =
taxableAnnualIncome - personalAllowance > 0
? taxableAnnualIncome - personalAllowance
: 0;
let basicRateTax = 0;
let higherRateTax = 0;
let additionalRateTax = 0;

// Income over £100k: the brackets need to move in accordance with personal allowance changes
const personalAllowanceDeduction =
DEFAULT_PERSONAL_ALLOWANCE - personalAllowance;
const higherBracket = HIGHER_BRACKET - personalAllowanceDeduction;
const additionalBracket = ADDITIONAL_BRACKET - personalAllowanceDeduction;

// Over £150k
if (taxableAnnualIncome > additionalBracket) {
// 3 rates apply (basic, higher, additional)
const additionalSection = taxableAnnualIncome - additionalBracket;
const higherSection =
taxableAnnualIncome - higherBracket - additionalSection;
const basicSection =
taxableAnnualIncome -
personalAllowance -
additionalSection -
higherSection;
basicRateTax = basicSection * BASIC_RATE;
higherRateTax = higherSection * HIGHER_RATE;
additionalRateTax = additionalSection * ADDITIONAL_RATE;
}

// Over £50k
if (
taxableAnnualIncome <= additionalBracket &&
taxableAnnualIncome > higherBracket
) {
// 2 bands apply (basic, higher)
const higherSection = taxableAnnualIncome - higherBracket;
const basicSection =
taxableAnnualIncome - personalAllowance - higherSection;
basicRateTax = basicSection * BASIC_RATE;
higherRateTax = higherSection * HIGHER_RATE;
}

// All salaries under £50k
if (
taxableAnnualIncome <= higherBracket &&
taxableAnnualIncome > personalAllowance
) {
// 1 band applies (basic)
basicRateTax = afterPersonalAllowance * BASIC_RATE;
}

return {
basicRateTax,
higherRateTax,
additionalRateTax,
};
};
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./incomeTax";
export * from "./nationalInsurance";
export * from "./personalAllowance";
export * from "./studentLoan";
35 changes: 35 additions & 0 deletions src/nationalInsurance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { getHmrcRates } from "./hmrc";
import type { TaxYear } from "./types";

// Calculates an individual's national insurance contributions based on taxable income
// Note: This is employee contributions only. Supports class 1, category A national insurance only.
// Uses the employee's weekly salary as a basis, as per the system, then re-converts into a year at the end.
// See https://www.gov.uk/national-insurance-rates-letters/category-letters for other categories
export const calculateEmployeeNationalInsurance = ({
taxYear,
taxableAnnualIncome,
}: {
taxYear?: TaxYear;
taxableAnnualIncome: number;
}) => {
const weeklySalary = taxableAnnualIncome / 52;
const { NI_MIDDLE_RATE, NI_UPPER_RATE, NI_MIDDLE_BRACKET, NI_UPPER_BRACKET } =
getHmrcRates(taxYear);
const afterFreeSection = weeklySalary - NI_MIDDLE_BRACKET;
let middleBracket = 0;
let upperBracket = 0;

if (weeklySalary > NI_UPPER_BRACKET) {
// 2 bands apply
const upperSection = weeklySalary - NI_UPPER_BRACKET;
const middleSection = weeklySalary - NI_MIDDLE_BRACKET - upperSection;
upperBracket = upperSection * NI_UPPER_RATE;
middleBracket = middleSection * NI_MIDDLE_RATE;
}

if (weeklySalary < NI_UPPER_BRACKET && weeklySalary > NI_MIDDLE_BRACKET) {
middleBracket = afterFreeSection * NI_MIDDLE_RATE;
}

return (middleBracket + upperBracket) * 52;
};
31 changes: 31 additions & 0 deletions src/personalAllowance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { TaxYear } from "./types";
import { getHmrcRates } from "./hmrc";

// Calculates an individual's annual personal allowance, based on taxable annual income.
// Not yet supported: marriage allowance, blind person's allowance
export const calculatePersonalAllowance = ({
taxYear,
taxableAnnualIncome,
}: {
taxYear?: TaxYear;
taxableAnnualIncome: number;
}): number => {
const { PERSONAL_ALLOWANCE_DROPOFF, DEFAULT_PERSONAL_ALLOWANCE } =
getHmrcRates(taxYear);

// £1 of personal allowance is reduced for every £2 of Income over £100,000
let personalAllowanceDeduction =
taxableAnnualIncome >= PERSONAL_ALLOWANCE_DROPOFF
? (taxableAnnualIncome - PERSONAL_ALLOWANCE_DROPOFF) / 2
: 0;

// When beyond £125k taxable income, the personal allowance will reach zero.
// Don't let the deduction go below zero, though.
if (personalAllowanceDeduction < 0) {
personalAllowanceDeduction = 0;
}

const sum = DEFAULT_PERSONAL_ALLOWANCE - personalAllowanceDeduction;

return sum < 0 ? 0 : sum;
};
35 changes: 35 additions & 0 deletions src/studentLoan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { StudentLoanPlan, TaxYear } from "./types";
import { getHmrcRates } from "./hmrc";

// Calculates an individual's annual student loan repayments
// Note that student loan repayments do not take into account the personal allowance in any way, it's a simple threshold system
export const calculateStudentLoanRepayments = ({
taxYear,
taxableAnnualIncome,
studentLoanPlanNo,
}: {
taxYear?: TaxYear;
taxableAnnualIncome: number;
studentLoanPlanNo: StudentLoanPlan;
}): number => {
const {
STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD,
STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD,
STUDENT_LOAN_REPAYMENT_AMOUNT,
} = getHmrcRates(taxYear);
let studentLoanAnnualRepayments = 0;

// Repayments are a % of income over HMRC-specified thresholds (threshold amount depends on plan number)
const threshold =
studentLoanPlanNo === 1
? STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD
: STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD;
const weeklySalary = taxableAnnualIncome / 52;

if (weeklySalary > threshold) {
studentLoanAnnualRepayments =
(weeklySalary - threshold) * STUDENT_LOAN_REPAYMENT_AMOUNT * 52;
}

return studentLoanAnnualRepayments;
};
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface IncomeTax {
basicRateTax: number;
higherRateTax: number;
additionalRateTax: number;
}

export type StudentLoanPlan = 1 | 2;

export type TaxYear = "2022/23";
Loading

0 comments on commit 90fa312

Please sign in to comment.