From 23d23ee27fe0a93af5bc1cd8b8c8de07af1c230b Mon Sep 17 00:00:00 2001 From: sgb-io Date: Thu, 30 Nov 2023 19:55:02 +0000 Subject: [PATCH] Complete support for all student loan plans --- README.md | 54 ++++++----- src/hmrc.ts | 14 ++- src/studentLoan.test.ts | 210 +++++++++++++++++++++++++++++++++++----- src/studentLoan.ts | 45 ++++++++- src/types.ts | 2 +- 5 files changed, 271 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 45d4cd8..429c04d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ There are 5 main APIs: - `calculatePersonalAllowance({ taxYear?: TaxYear, taxableAnnualIncome: number })`: calculates an individual's personal allowance for a tax year, single amount. - `calculateIncomeTax({ taxYear?: TaxYear, personalAllowance: number, taxableAnnualIncome: number })`: 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({ taxYear?: TaxYear, taxableAnnualIncome: number })`: 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({ taxYear?: TaxYear, taxableAnnualIncome: number, studentLoanPlanNo: number })`: calculates the student loan repayments due in a tax year on an individual's taxable income, single amount. `studentLoanPlanNo` can be `1` or `2`. +- `calculateStudentLoanRepayments({ taxYear?: TaxYear, taxableAnnualIncome: number, studentLoanPlanNo: number })`: calculates the student loan repayments due in a tax year on an individual's taxable income, single amount. `studentLoanPlanNo` can be `1`, `2`, `4`, `5` or `postgrad`. - `getHmrcRates({ taxYear?: TaxYear })`: returns an underlying static set of HMRC rates for a given tax year. This is useful for doing your own arbitrary calculations. All APIs return raw amounts and there is no formatting or display functionality. @@ -37,34 +37,42 @@ import { calculateIncomeTax, calculateEmployeeNationalInsurance, calculateStudentLoanRepayments, -} from '@saving-tool/hmrc-income-tax'; +} from "@saving-tool/hmrc-income-tax"; // Mark S. -const taxableAnnualIncome = 53_900 +const taxableAnnualIncome = 53_900; const personalAllowance = calculatePersonalAllowance({ taxableAnnualIncome }); // => 12570 -const incomeTax = calculateIncomeTax({ personalAllowance, taxableAnnualIncome }); +const incomeTax = calculateIncomeTax({ + personalAllowance, + taxableAnnualIncome, +}); const { basicRateTax, higherRateTax, additionalRateTax } = incomeTax; const totalIncomeTax = basicRateTax + higherRateTax + additionalRateTax; // => 8992 -const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ taxableAnnualIncome }); +const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ + taxableAnnualIncome, +}); // => 5471 -const studentLoanRepayments = calculateStudentLoanRepayments({ taxableAnnualIncome, studentLoanPlanNo: 1 }); +const studentLoanRepayments = calculateStudentLoanRepayments({ + taxableAnnualIncome, + studentLoanPlanNo: 1, +}); // => 3162 // Do whatever you want, e.g. calculate the take-home pay -const takeHome = taxableAnnualIncome - - totalIncomeTax - - nationalInsuranceContributions - - studentLoanRepayments; +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 @@ -72,31 +80,34 @@ import { calculatePersonalAllowance, calculateIncomeTax, calculateEmployeeNationalInsurance, -} from '@saving-tool/hmrc-income-tax'; +} from "@saving-tool/hmrc-income-tax"; // Irv B. -const taxableAnnualIncome = 160_000 +const taxableAnnualIncome = 160_000; const personalAllowance = calculatePersonalAllowance({ taxableAnnualIncome }); // => 0 -const incomeTax = calculateIncomeTax({ personalAllowance, taxableAnnualIncome }); +const incomeTax = calculateIncomeTax({ + personalAllowance, + taxableAnnualIncome, +}); const { basicRateTax, higherRateTax, additionalRateTax } = incomeTax; const totalIncomeTax = basicRateTax + higherRateTax + additionalRateTax; // => 57589 -const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ taxableAnnualIncome }); +const nationalInsuranceContributions = calculateEmployeeNationalInsurance({ + taxableAnnualIncome, +}); // => 8919 // Do whatever you want, e.g. calculate the take-home pay -const takeHome = taxableAnnualIncome - - totalIncomeTax - - nationalInsuranceContributions; +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. - +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 @@ -117,5 +128,4 @@ const gbpFormatter = new Intl.NumberFormat("en-GB", { export const roundAndFormatGbp = (amount: number) => { return formatGbp(Math.round(amount)); }; - -``` \ No newline at end of file +``` diff --git a/src/hmrc.ts b/src/hmrc.ts index c9b2b27..fdccdb5 100644 --- a/src/hmrc.ts +++ b/src/hmrc.ts @@ -16,7 +16,11 @@ interface TaxRates { // Previous rates: https://www.gov.uk/guidance/previous-annual-repayment-thresholds STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD: number; STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD: number; + STUDENT_LOAN_PLAN_4_WEEKLY_THRESHOLD: number; + STUDENT_LOAN_PLAN_5_WEEKLY_THRESHOLD: number; + STUDENT_LOAN_POSTGRAD_WEEKLY_THRESHOLD: number; STUDENT_LOAN_REPAYMENT_AMOUNT: number; + STUDENT_LOAN_REPAYMENT_AMOUNT_POSTGRAD: number; // National Insurance // See https://www.gov.uk/guidance/rates-and-thresholds-for-employers-2022-to-2023 for current and previous rates @@ -41,7 +45,11 @@ const taxRates: Record = { // Student loan repayments STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD: 388, STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD: 524, + STUDENT_LOAN_PLAN_4_WEEKLY_THRESHOLD: 487.98, + STUDENT_LOAN_PLAN_5_WEEKLY_THRESHOLD: 480, // Note: this was only introduced in 2023/24, so technically isn't relevant to 22/23 + STUDENT_LOAN_POSTGRAD_WEEKLY_THRESHOLD: 403.84, STUDENT_LOAN_REPAYMENT_AMOUNT: 0.09, // People on plans 1 or 2 repay 9% of the amount you earn over the threshold + STUDENT_LOAN_REPAYMENT_AMOUNT_POSTGRAD: 0.06, // People on postgrad plans repay 6% of the amount you earn over the threshold // National Insurance NI_MIDDLE_RATE: 0.1325, NI_UPPER_RATE: 0.0325, @@ -61,7 +69,11 @@ const taxRates: Record = { // Student loan repayments STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD: 423, STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD: 524, - STUDENT_LOAN_REPAYMENT_AMOUNT: 0.09, // People on plans 1 or 2 repay 9% of the amount you earn over the threshold + STUDENT_LOAN_PLAN_4_WEEKLY_THRESHOLD: 532, + STUDENT_LOAN_PLAN_5_WEEKLY_THRESHOLD: 480, + STUDENT_LOAN_POSTGRAD_WEEKLY_THRESHOLD: 403, + STUDENT_LOAN_REPAYMENT_AMOUNT: 0.09, // People on plans 1, 2, 4 + 5 repay 9% of the amount you earn over the threshold + STUDENT_LOAN_REPAYMENT_AMOUNT_POSTGRAD: 0.06, // People on postgrad plans repay 6% of the amount you earn over the threshold // National Insurance NI_MIDDLE_RATE: 0.12, NI_UPPER_RATE: 0.02, diff --git a/src/studentLoan.test.ts b/src/studentLoan.test.ts index f528751..0bfbfd5 100644 --- a/src/studentLoan.test.ts +++ b/src/studentLoan.test.ts @@ -1,31 +1,31 @@ import { calculateStudentLoanRepayments } from "./studentLoan"; -const expectationsPlan1 = [ - { taxableAnnualIncome: 15_000, repayments: 0 }, - { taxableAnnualIncome: 17_500, repayments: 0 }, - { taxableAnnualIncome: 20_000, repayments: 0 }, - { taxableAnnualIncome: 22_500, repayments: 209.1599999999999 }, - { taxableAnnualIncome: 25_000, repayments: 434.16 }, - { taxableAnnualIncome: 50_000, repayments: 2684.16 }, - { taxableAnnualIncome: 55_000, repayments: 3134.1599999999994 }, - { taxableAnnualIncome: 60_000, repayments: 3584.1599999999994 }, - { taxableAnnualIncome: 75_000, repayments: 4934.16 }, - { taxableAnnualIncome: 90_000, repayments: 6284.16 }, - { taxableAnnualIncome: 110_000, repayments: 8084.159999999999 }, - { taxableAnnualIncome: 120_000, repayments: 8984.16 }, - { taxableAnnualIncome: 124_500, repayments: 9389.16 }, - { taxableAnnualIncome: 125_000, repayments: 9434.159999999998 }, - { taxableAnnualIncome: 130_000, repayments: 9884.16 }, - { taxableAnnualIncome: 145_000, repayments: 11234.16 }, - { taxableAnnualIncome: 160_000, repayments: 12584.160000000002 }, - { taxableAnnualIncome: 175_000, repayments: 13934.159999999998 }, - { taxableAnnualIncome: 200_000, repayments: 16184.160000000002 }, - { taxableAnnualIncome: 250_000, repayments: 20684.16 }, - { taxableAnnualIncome: 500_000, repayments: 43184.159999999996 }, - { taxableAnnualIncome: 1_000_000, repayments: 88184.15999999999 }, -]; - describe("calculateStudentLoanRepayments", () => { + const expectationsPlan1 = [ + { taxableAnnualIncome: 15_000, repayments: 0 }, + { taxableAnnualIncome: 17_500, repayments: 0 }, + { taxableAnnualIncome: 20_000, repayments: 0 }, + { taxableAnnualIncome: 22_500, repayments: 209.1599999999999 }, + { taxableAnnualIncome: 25_000, repayments: 434.16 }, + { taxableAnnualIncome: 50_000, repayments: 2684.16 }, + { taxableAnnualIncome: 55_000, repayments: 3134.1599999999994 }, + { taxableAnnualIncome: 60_000, repayments: 3584.1599999999994 }, + { taxableAnnualIncome: 75_000, repayments: 4934.16 }, + { taxableAnnualIncome: 90_000, repayments: 6284.16 }, + { taxableAnnualIncome: 110_000, repayments: 8084.159999999999 }, + { taxableAnnualIncome: 120_000, repayments: 8984.16 }, + { taxableAnnualIncome: 124_500, repayments: 9389.16 }, + { taxableAnnualIncome: 125_000, repayments: 9434.159999999998 }, + { taxableAnnualIncome: 130_000, repayments: 9884.16 }, + { taxableAnnualIncome: 145_000, repayments: 11234.16 }, + { taxableAnnualIncome: 160_000, repayments: 12584.160000000002 }, + { taxableAnnualIncome: 175_000, repayments: 13934.159999999998 }, + { taxableAnnualIncome: 200_000, repayments: 16184.160000000002 }, + { taxableAnnualIncome: 250_000, repayments: 20684.16 }, + { taxableAnnualIncome: 500_000, repayments: 43184.159999999996 }, + { taxableAnnualIncome: 1_000_000, repayments: 88184.15999999999 }, + ]; + describe("Plan 1 loans", () => { expectationsPlan1.forEach((expectation) => { const { taxableAnnualIncome, repayments } = expectation; @@ -40,4 +40,164 @@ describe("calculateStudentLoanRepayments", () => { }); }); }); + + const expectationsPlan2 = [ + { taxableAnnualIncome: 15_000, repayments: 0 }, + { taxableAnnualIncome: 17_500, repayments: 0 }, + { taxableAnnualIncome: 20_000, repayments: 0 }, + { taxableAnnualIncome: 22_500, repayments: 0 }, + { taxableAnnualIncome: 25_000, repayments: 0 }, + { taxableAnnualIncome: 50_000, repayments: 2047.6799999999998 }, + { taxableAnnualIncome: 55_000, repayments: 2497.6799999999994 }, + { taxableAnnualIncome: 60_000, repayments: 2947.68 }, + { taxableAnnualIncome: 75_000, repayments: 4297.68 }, + { taxableAnnualIncome: 90_000, repayments: 5647.679999999999 }, + { taxableAnnualIncome: 110_000, repayments: 7447.6799999999985 }, + { taxableAnnualIncome: 120_000, repayments: 8347.68 }, + { taxableAnnualIncome: 124_500, repayments: 8752.679999999998 }, + { taxableAnnualIncome: 125_000, repayments: 8797.68 }, + { taxableAnnualIncome: 130_000, repayments: 9247.68 }, + { taxableAnnualIncome: 145_000, repayments: 10597.68 }, + { taxableAnnualIncome: 160_000, repayments: 11947.68 }, + { taxableAnnualIncome: 175_000, repayments: 13297.679999999998 }, + { taxableAnnualIncome: 200_000, repayments: 15547.68 }, + { taxableAnnualIncome: 250_000, repayments: 20047.679999999997 }, + { taxableAnnualIncome: 500_000, repayments: 42547.68 }, + { taxableAnnualIncome: 1_000_000, repayments: 87547.68 }, + ]; + + describe("Plan 2 loans", () => { + expectationsPlan2.forEach((expectation) => { + const { taxableAnnualIncome, repayments } = expectation; + test(taxableAnnualIncome.toString(), () => { + expect( + calculateStudentLoanRepayments({ + taxYear: "2022/23", + taxableAnnualIncome, + studentLoanPlanNo: 2, + }) + ).toEqual(repayments); + }); + }); + }); + + const expectationsPlan4 = [ + { taxableAnnualIncome: 15_000, repayments: 0 }, + { taxableAnnualIncome: 17_500, repayments: 0 }, + { taxableAnnualIncome: 20_000, repayments: 0 }, + { taxableAnnualIncome: 22_500, repayments: 0 }, + { taxableAnnualIncome: 25_000, repayments: 0 }, + { taxableAnnualIncome: 50_000, repayments: 2216.2536 }, + { taxableAnnualIncome: 55_000, repayments: 2666.2535999999996 }, + { taxableAnnualIncome: 60_000, repayments: 3116.2535999999996 }, + { taxableAnnualIncome: 75_000, repayments: 4466.2536 }, + { taxableAnnualIncome: 90_000, repayments: 5816.2536 }, + { taxableAnnualIncome: 110_000, repayments: 7616.253599999999 }, + { taxableAnnualIncome: 120_000, repayments: 8516.253599999998 }, + { taxableAnnualIncome: 124_500, repayments: 8921.2536 }, + { taxableAnnualIncome: 125_000, repayments: 8966.253599999998 }, + { taxableAnnualIncome: 130_000, repayments: 9416.2536 }, + { taxableAnnualIncome: 145_000, repayments: 10766.2536 }, + { taxableAnnualIncome: 160_000, repayments: 12116.2536 }, + { taxableAnnualIncome: 175_000, repayments: 13466.253599999998 }, + { taxableAnnualIncome: 200_000, repayments: 15716.253599999998 }, + { taxableAnnualIncome: 250_000, repayments: 20216.253599999996 }, + { taxableAnnualIncome: 500_000, repayments: 42716.253600000004 }, + { taxableAnnualIncome: 1_000_000, repayments: 87716.2536 }, + ]; + + describe("Plan 4 loans", () => { + expectationsPlan4.forEach((expectation) => { + const { taxableAnnualIncome, repayments } = expectation; + test(taxableAnnualIncome.toString(), () => { + expect( + calculateStudentLoanRepayments({ + taxYear: "2022/23", + taxableAnnualIncome, + studentLoanPlanNo: 4, + }) + ).toEqual(repayments); + }); + }); + }); + + const expectationsPlan5 = [ + { taxableAnnualIncome: 15_000, repayments: 0 }, + { taxableAnnualIncome: 17_500, repayments: 0 }, + { taxableAnnualIncome: 20_000, repayments: 0 }, + { taxableAnnualIncome: 22_500, repayments: 0 }, + { taxableAnnualIncome: 25_000, repayments: 3.6000000000000205 }, + { taxableAnnualIncome: 50_000, repayments: 2253.6 }, + { taxableAnnualIncome: 55_000, repayments: 2703.5999999999995 }, + { taxableAnnualIncome: 60_000, repayments: 3153.6 }, + { taxableAnnualIncome: 75_000, repayments: 4503.599999999999 }, + { taxableAnnualIncome: 90_000, repayments: 5853.599999999999 }, + { taxableAnnualIncome: 110_000, repayments: 7653.599999999999 }, + { taxableAnnualIncome: 120_000, repayments: 8553.6 }, + { taxableAnnualIncome: 124_500, repayments: 8958.599999999999 }, + { taxableAnnualIncome: 125_000, repayments: 9003.599999999999 }, + { taxableAnnualIncome: 130_000, repayments: 9453.599999999999 }, + { taxableAnnualIncome: 145_000, repayments: 10803.6 }, + { taxableAnnualIncome: 160_000, repayments: 12153.6 }, + { taxableAnnualIncome: 175_000, repayments: 13503.599999999999 }, + { taxableAnnualIncome: 200_000, repayments: 15753.599999999999 }, + { taxableAnnualIncome: 250_000, repayments: 20253.6 }, + { taxableAnnualIncome: 500_000, repayments: 42753.6 }, + { taxableAnnualIncome: 1_000_000, repayments: 87753.59999999999 }, + ]; + + describe("Plan 5 loans", () => { + expectationsPlan5.forEach((expectation) => { + const { taxableAnnualIncome, repayments } = expectation; + test(taxableAnnualIncome.toString(), () => { + expect( + calculateStudentLoanRepayments({ + taxYear: "2022/23", + taxableAnnualIncome, + studentLoanPlanNo: 5, + }) + ).toEqual(repayments); + }); + }); + }); + + const expectationsPostgrad = [ + { taxableAnnualIncome: 15_000, repayments: 0 }, + { taxableAnnualIncome: 17_500, repayments: 0 }, + { taxableAnnualIncome: 20_000, repayments: 0 }, + { taxableAnnualIncome: 22_500, repayments: 90.01920000000004 }, + { taxableAnnualIncome: 25_000, repayments: 240.0192000000001 }, + { taxableAnnualIncome: 50_000, repayments: 1740.0192 }, + { taxableAnnualIncome: 55_000, repayments: 2040.0192000000002 }, + { taxableAnnualIncome: 60_000, repayments: 2340.0192 }, + { taxableAnnualIncome: 75_000, repayments: 3240.0192 }, + { taxableAnnualIncome: 90_000, repayments: 4140.019200000001 }, + { taxableAnnualIncome: 110_000, repayments: 5340.0192 }, + { taxableAnnualIncome: 120_000, repayments: 5940.0192 }, + { taxableAnnualIncome: 124_500, repayments: 6210.0192 }, + { taxableAnnualIncome: 125_000, repayments: 6240.0192 }, + { taxableAnnualIncome: 130_000, repayments: 6540.019199999999 }, + { taxableAnnualIncome: 145_000, repayments: 7440.019199999999 }, + { taxableAnnualIncome: 160_000, repayments: 8340.019199999999 }, + { taxableAnnualIncome: 175_000, repayments: 9240.019199999999 }, + { taxableAnnualIncome: 200_000, repayments: 10740.019199999999 }, + { taxableAnnualIncome: 250_000, repayments: 13740.019199999999 }, + { taxableAnnualIncome: 500_000, repayments: 28740.0192 }, + { taxableAnnualIncome: 1_000_000, repayments: 58740.01919999999 }, + ]; + + describe("Postgrad loans", () => { + expectationsPostgrad.forEach((expectation) => { + const { taxableAnnualIncome, repayments } = expectation; + test(taxableAnnualIncome.toString(), () => { + expect( + calculateStudentLoanRepayments({ + taxYear: "2022/23", + taxableAnnualIncome, + studentLoanPlanNo: "postgrad", + }) + ).toEqual(repayments); + }); + }); + }); }); diff --git a/src/studentLoan.ts b/src/studentLoan.ts index 2f42f5f..65a3ea0 100644 --- a/src/studentLoan.ts +++ b/src/studentLoan.ts @@ -15,20 +15,55 @@ export const calculateStudentLoanRepayments = ({ const { STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD, STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD, + STUDENT_LOAN_PLAN_4_WEEKLY_THRESHOLD, + STUDENT_LOAN_PLAN_5_WEEKLY_THRESHOLD, + STUDENT_LOAN_POSTGRAD_WEEKLY_THRESHOLD, STUDENT_LOAN_REPAYMENT_AMOUNT, + STUDENT_LOAN_REPAYMENT_AMOUNT_POSTGRAD, } = 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; + + let threshold: number | undefined; + + switch (studentLoanPlanNo) { + case 1: { + threshold = STUDENT_LOAN_PLAN_1_WEEKLY_THRESHOLD; + break; + } + case 2: { + threshold = STUDENT_LOAN_PLAN_2_WEEKLY_THRESHOLD; + break; + } + case 4: { + threshold = STUDENT_LOAN_PLAN_4_WEEKLY_THRESHOLD; + break; + } + case 5: { + threshold = STUDENT_LOAN_PLAN_5_WEEKLY_THRESHOLD; + break; + } + case "postgrad": { + threshold = STUDENT_LOAN_POSTGRAD_WEEKLY_THRESHOLD; + break; + } + default: { + throw new Error( + `Student loan plan must be one of: 1, 2, 4, 5 or 'postgrad' (was: ${studentLoanPlanNo})` + ); + } + } + const weeklySalary = taxableAnnualIncome / 52; + const repaymentAmount = + studentLoanPlanNo === "postgrad" + ? STUDENT_LOAN_REPAYMENT_AMOUNT_POSTGRAD + : STUDENT_LOAN_REPAYMENT_AMOUNT; if (weeklySalary > threshold) { studentLoanAnnualRepayments = - (weeklySalary - threshold) * STUDENT_LOAN_REPAYMENT_AMOUNT * 52; + (weeklySalary - threshold) * repaymentAmount * 52; } return studentLoanAnnualRepayments; diff --git a/src/types.ts b/src/types.ts index 3a909e6..674c2a6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,6 @@ export interface IncomeTax { additionalRateTax: number; } -export type StudentLoanPlan = 1 | 2; +export type StudentLoanPlan = 1 | 2 | 4 | 5 | "postgrad"; export type TaxYear = "2022/23" | "2023/24";