Skip to content

Commit

Permalink
Add calculatePensionAnnualAllowance including support for Pension Tap…
Browse files Browse the repository at this point in the history
…ering calculations (#23)

* Initial implementation

* Add tests and tips

* Update docs, expose a param

* Run prettier
  • Loading branch information
sgb-io authored Sep 11, 2024
1 parent b2ed0cc commit 0237118
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 6 deletions.
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ Run: `yarn add @saving-tool/hmrc-income-tax` (or `npm install @saving-tool/hmrc-

## Usage

There are 5 functions exposed by the library:
`country` is an optional input for all APIs: `"England/NI/Wales" | "Scotland"`. If not provided, the default is `"England/NI/Wales"`.

Note that `taxYear` is an optional input to select which tax year rates should be used (default is "2024/25").

### `calculatePersonalAllowance`

Expand Down Expand Up @@ -92,11 +94,42 @@ getHmrcRates({
}) => EnglishTaxRates | ScottishTaxRates;
```

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

`country` is an optional input for all APIs: `"England/NI/Wales" | "Scotland"`. If not provided, the default is `"England/NI/Wales"`.
Returns an object containing an annual allowance information for pension contributions.
Note that pension tapering calculations are quite complex. You can also refer to the tests with various examples.

Note that `taxYear` is an optional input to select which tax year rates should be used (default is "2024/25").
"Personally paid" pension contributions means you paid from your bank account direct to a pension i.e. not through work.

For employee pension contributions:

- Use `employeeDcPensionContributions` for salary sacrifice contributions (post-2015 schemes\*)
- Use `retrospectivePensionPaymentsTaxRelief` for salary sacrifice contributions (pre-2015 schemes\*)
- Use `retrospectivePensionPaymentsTaxRelief` for personally paid or other relief-at-source contributions

\* The rules changed for salary sacrifice schemes set up on or after 9th July 2015

```typescript
calculatePensionAnnualAllowance({
taxYear?: TaxYear;
totalAnnualIncome: number; // Note: include any salary sacrificed income, plus investent income, but do not include employer contributions, or relief-at-source contributions such as ones personally paid
retrospectivePensionPaymentsTaxRelief?: number; // Relief-at-source pension contributions such as ones personally paid
employeeDcPensionContributions?: number;
employerDcPensionContributions?: number;
lumpSumDeathBenefits?: number;
}) => {
adjustedIncome: number;
thresholdIncome: number;
reduction: number;
allowance: number;
};
```

Note that this implementation does not yet support the following:

- Use of carry-forward from previous years' allowances
- Accounting for DB (Defined Benefit) pensions
- Paying into overseas pensions

## Examples (2022/23 HMRC Rates)

Expand Down
18 changes: 17 additions & 1 deletion src/hmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const englandNiWalesTaxRates: Record<SupportedEnglishTaxYear, EnglishTaxRates> =
NI_UPPER_RATE: 0.0325,
NI_MIDDLE_BRACKET: 242,
NI_UPPER_BRACKET: 967,
// Pension allowances
PENSION_ANNUAL_ALLOWANCE: 40_000,
PENSION_MINIMUM_ANNUAL_ALLOWANCE: 4_000,
PENSION_ADJUSTED_LIMIT: 240_000,
},
"2023/24": {
COUNTRY: "England/NI/Wales",
Expand All @@ -57,6 +61,10 @@ const englandNiWalesTaxRates: Record<SupportedEnglishTaxYear, EnglishTaxRates> =
NI_UPPER_RATE: 0.02,
NI_MIDDLE_BRACKET: 242,
NI_UPPER_BRACKET: 967,
// Pension allowances
PENSION_ANNUAL_ALLOWANCE: 60_000,
PENSION_MINIMUM_ANNUAL_ALLOWANCE: 10_000,
PENSION_ADJUSTED_LIMIT: 260_000,
},
"2024/25": {
COUNTRY: "England/NI/Wales",
Expand All @@ -81,6 +89,10 @@ const englandNiWalesTaxRates: Record<SupportedEnglishTaxYear, EnglishTaxRates> =
NI_UPPER_RATE: 0.02,
NI_MIDDLE_BRACKET: 242,
NI_UPPER_BRACKET: 967,
// Pension allowances
PENSION_ANNUAL_ALLOWANCE: 60_000,
PENSION_MINIMUM_ANNUAL_ALLOWANCE: 10_000,
PENSION_ADJUSTED_LIMIT: 260_000,
},
};

Expand Down Expand Up @@ -118,6 +130,10 @@ const scottishTaxRates: Record<SupportedScottishTaxYear, ScottishTaxRates> = {
NI_UPPER_RATE: 0.02,
NI_MIDDLE_BRACKET: 242,
NI_UPPER_BRACKET: 967,
// Pension allowances
PENSION_ANNUAL_ALLOWANCE: 60_000,
PENSION_MINIMUM_ANNUAL_ALLOWANCE: 10_000,
PENSION_ADJUSTED_LIMIT: 260_000,
},
};

Expand All @@ -136,7 +152,7 @@ export const getHmrcRates = (
if (!taxRates.hasOwnProperty(taxYearToUse)) {
throw new Error(
`Tax Year ${taxYearToUse} is not currently supported for ${
options.country ?? "England/NI/Wales"
options?.country ?? "England/NI/Wales"
}`
);
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from "./incomeTax";
export * from "./nationalInsurance";
export * from "./personalAllowance";
export * from "./studentLoan";
export * from "./pensionAnnualAllowance";
166 changes: 166 additions & 0 deletions src/pensionAnnualAllowance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { calculatePensionAnnualAllowance } from "./pensionAnnualAllowance";

describe("calculatePensionAnnualAllowance (24/25)", () => {
test("Fidelity Example 1", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 210_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 20_000,
});

expect(result).toEqual({
adjustedIncome: 230_000,
thresholdIncome: 210_000,
reduction: 0,
allowance: 60_000,
});
});

test("Fidelity Example 2", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 235_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 60_000,
});

expect(result).toEqual({
adjustedIncome: 295_000,
thresholdIncome: 235_000,
reduction: 17_500,
allowance: 42_500,
});
});

test("AJ Bell Example 1", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 200_000,
retrospectivePensionPaymentsTaxRelief: 20_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 20_000,
});

expect(result).toEqual({
adjustedIncome: 220_000,
thresholdIncome: 180_000,
reduction: 0,
allowance: 60_000,
});
});

test("AJ Bell Example 2", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 230_000,
retrospectivePensionPaymentsTaxRelief: 10_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 50_000,
});

expect(result).toEqual({
adjustedIncome: 280_000,
thresholdIncome: 220_000,
reduction: 10_000,
allowance: 50_000,
});
});

describe("Quilter Examples", () => {
test("Example A", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 265_000,
retrospectivePensionPaymentsTaxRelief: 0,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 0,
});

expect(result).toEqual({
adjustedIncome: 265_000,
thresholdIncome: 265_000,
reduction: 2500,
allowance: 57_500,
});
});

test("Example B", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 330_000,
retrospectivePensionPaymentsTaxRelief: 20_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 0,
});

expect(result).toEqual({
adjustedIncome: 330_000,
thresholdIncome: 310_000,
reduction: 35_000,
allowance: 25_000,
});
});

test("Example C", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 245_000,
retrospectivePensionPaymentsTaxRelief: 0,
employeeDcPensionContributions: 20_000,
employerDcPensionContributions: 0,
});

expect(result).toEqual({
adjustedIncome: 265_000,
thresholdIncome: 265_000,
reduction: 2_500,
allowance: 57_500,
});
});
});

describe("Royal London Examples", () => {
// Examples that use carry forward or DB pensions are not included
test("Example 1", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 284_000,
retrospectivePensionPaymentsTaxRelief: 15_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 30_000,
});

expect(result).toEqual({
adjustedIncome: 314_000,
thresholdIncome: 269_000,
reduction: 27_000,
allowance: 33_000,
});
});

test("Example 5", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 270_000,
retrospectivePensionPaymentsTaxRelief: 0,
employeeDcPensionContributions: 20_000,
employerDcPensionContributions: 20_000,
});

expect(result).toEqual({
adjustedIncome: 310_000,
thresholdIncome: 290_000,
reduction: 25_000,
allowance: 35_000,
});
});
});

test("HL example", () => {
const result = calculatePensionAnnualAllowance({
totalAnnualIncome: 250_000,
retrospectivePensionPaymentsTaxRelief: 24_000,
employeeDcPensionContributions: 0,
employerDcPensionContributions: 36_000,
});

expect(result).toEqual({
adjustedIncome: 286_000,
thresholdIncome: 226_000,
reduction: 13_000,
allowance: 47_000,
});
});
});
75 changes: 75 additions & 0 deletions src/pensionAnnualAllowance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { getHmrcRates } from "./hmrc";

import type { TaxYear } from "./types";

interface Args {
taxYear?: TaxYear;
totalAnnualIncome: number;
retrospectivePensionPaymentsTaxRelief?: number;
employeeDcPensionContributions?: number;
employerDcPensionContributions?: number;
lumpSumDeathBenefits?: number;
}

// Calculates an individual's annual pension contributions allowance
// UNSUPPORTED: DB Pensions
// UNSUPPORTED: Carry forward allowances from previous years
// UNSUPPORTED: Paying into overseas pension schemes
// Tips:
// Use `retrospectivePensionPaymentsTaxRelief` for personal contributions (or any other form of relief at source contribution)
// Use `employeeDcPensionContributions` for post-2015 Salary Sacrifice schemes
// Use `retrospectivePensionPaymentsTaxRelief` for pre-2015 Salary Sacrifice schemes
export const calculatePensionAnnualAllowance = ({
taxYear,
totalAnnualIncome,
retrospectivePensionPaymentsTaxRelief,
employeeDcPensionContributions,
employerDcPensionContributions,
lumpSumDeathBenefits,
}: Args) => {
const {
PENSION_ANNUAL_ALLOWANCE, // 60k
PENSION_MINIMUM_ANNUAL_ALLOWANCE, // 10k
PENSION_ADJUSTED_LIMIT, // 260k
} = getHmrcRates({ taxYear });

const pensionSavings =
(employeeDcPensionContributions ?? 0) +
(employerDcPensionContributions ?? 0) +
(retrospectivePensionPaymentsTaxRelief ?? 0);

const adjustedIncome =
totalAnnualIncome +
pensionSavings -
(retrospectivePensionPaymentsTaxRelief ?? 0) -
(lumpSumDeathBenefits ?? 0);

const thresholdIncome =
totalAnnualIncome +
(employeeDcPensionContributions ?? 0) -
(retrospectivePensionPaymentsTaxRelief ?? 0) -
(lumpSumDeathBenefits ?? 0);

const amountOver = adjustedIncome - PENSION_ADJUSTED_LIMIT;

// Reduction is £1 per £2 over the limit
// Also, reductions are rounded down to the nearest £1
const reduction = amountOver > 0 ? Math.floor(amountOver / 2) : 0;

let newAllowance = PENSION_ANNUAL_ALLOWANCE;

if (reduction > 0) {
const updated = PENSION_ANNUAL_ALLOWANCE - reduction;
newAllowance =
updated < PENSION_MINIMUM_ANNUAL_ALLOWANCE
? PENSION_MINIMUM_ANNUAL_ALLOWANCE
: updated;
}

return {
adjustedIncome,
thresholdIncome,
reduction,
allowance: newAllowance,
};
};
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ interface BasicTaxRates {
NI_UPPER_RATE: number;
NI_MIDDLE_BRACKET: number;
NI_UPPER_BRACKET: number;

// Pension allowances
PENSION_ANNUAL_ALLOWANCE: number;
PENSION_MINIMUM_ANNUAL_ALLOWANCE: number;
PENSION_ADJUSTED_LIMIT: number;
}

export interface EnglishTaxRates extends BasicTaxRates {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"compilerOptions": {
"outDir": "lib",
"declaration": true
"declaration": true,
"strictNullChecks": true
},
"include": ["src/**/*.ts"],
"exclude": []
Expand Down

0 comments on commit 0237118

Please sign in to comment.