-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Import and refactor code from SavingTool source
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
Showing
13 changed files
with
909 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "eslint:recommended" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
yarn-error.log | ||
lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"typescript.tsdk": "node_modules\\typescript\\lib" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}; | ||
|
||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
Oops, something went wrong.