From 29b4851aab46ed1f25ceab9b66885bfd29036fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 5 Dec 2024 16:43:45 +0000 Subject: [PATCH] feat: Calculate VAT using `getFeeBreakdown()` (#579) --- src/types/feeBreakdown.ts | 2 +- src/utils/feeBreakdown.test.ts | 50 ++++++++++++++++++++++------------ src/utils/feeBreakdown.ts | 22 ++++++++++++--- src/utils/index.ts | 2 +- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/types/feeBreakdown.ts b/src/types/feeBreakdown.ts index c617b046..b0a68f02 100644 --- a/src/types/feeBreakdown.ts +++ b/src/types/feeBreakdown.ts @@ -12,7 +12,7 @@ export interface FeeBreakdown { export interface PassportFeeFields { "application.fee.calculated": number; "application.fee.payable": number; - "application.fee.payable.vat": number; + "application.fee.payable.includesVAT": boolean; "application.fee.reduction.alternative": boolean; "application.fee.reduction.parishCouncil": boolean; "application.fee.reduction.sports": boolean; diff --git a/src/utils/feeBreakdown.test.ts b/src/utils/feeBreakdown.test.ts index 481dc7a1..cd1a6f10 100644 --- a/src/utils/feeBreakdown.test.ts +++ b/src/utils/feeBreakdown.test.ts @@ -27,7 +27,7 @@ describe("calculateReduction() helper function", () => { const input: PassportFeeFields = { "application.fee.calculated": 100, "application.fee.payable": 50, - "application.fee.payable.vat": 0, + "application.fee.payable.includesVAT": false, "application.fee.reduction.alternative": false, "application.fee.reduction.parishCouncil": false, "application.fee.reduction.sports": false, @@ -43,7 +43,7 @@ describe("calculateReduction() helper function", () => { const input: PassportFeeFields = { "application.fee.calculated": 0, "application.fee.payable": 100, - "application.fee.payable.vat": 0, + "application.fee.payable.includesVAT": false, "application.fee.reduction.alternative": false, "application.fee.reduction.parishCouncil": false, "application.fee.reduction.sports": false, @@ -61,7 +61,7 @@ describe("toFeeBreakdown() helper function", () => { const input: PassportFeeFields = { "application.fee.calculated": 100, "application.fee.payable": 50, - "application.fee.payable.vat": 10, + "application.fee.payable.includesVAT": true, "application.fee.reduction.alternative": false, "application.fee.reduction.parishCouncil": false, "application.fee.reduction.sports": false, @@ -73,14 +73,13 @@ describe("toFeeBreakdown() helper function", () => { expect(amount.calculated).toEqual(input["application.fee.calculated"]); expect(amount.payable).toEqual(input["application.fee.payable"]); - expect(amount.vat).toEqual(input["application.fee.payable.vat"]); expect(amount.reduction).toEqual(50); }); it("sets calculated to payable amount if no calculated value is provided", () => { const input: PassportFeeFields = { "application.fee.calculated": 0, - "application.fee.payable.vat": 10, + "application.fee.payable.includesVAT": true, "application.fee.payable": 50, "application.fee.reduction.alternative": false, "application.fee.reduction.parishCouncil": false, @@ -93,6 +92,23 @@ describe("toFeeBreakdown() helper function", () => { expect(amount.calculated).toEqual(input["application.fee.payable"]); }); + + it("correctly calculates the VAT", () => { + const input: PassportFeeFields = { + "application.fee.calculated": 100, + "application.fee.payable": 50, + "application.fee.payable.includesVAT": true, + "application.fee.reduction.alternative": false, + "application.fee.reduction.parishCouncil": false, + "application.fee.reduction.sports": false, + "application.fee.exemption.disability": false, + "application.fee.exemption.resubmission": false, + }; + + const { amount } = toFeeBreakdown(input); + + expect(amount.vat).toEqual(16.67); + }); }); describe("getFeeBreakdown() function", () => { @@ -101,7 +117,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "some.other.fields": ["abc", "xyz"], }; @@ -112,7 +128,7 @@ describe("getFeeBreakdown() function", () => { calculated: 1000, payable: 800, reduction: 200, - vat: 160, + vat: 166.67, }, exemptions: [], reductions: [], @@ -123,7 +139,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": [1000], "application.fee.payable": [800], - "application.fee.payable.vat": [160], + "application.fee.payable.includesVAT": ["true"], "some.other.fields": ["abc", "xyz"], }; @@ -134,7 +150,7 @@ describe("getFeeBreakdown() function", () => { calculated: 1000, payable: 800, reduction: 200, - vat: 160, + vat: 166.67, }, exemptions: [], reductions: [], @@ -145,7 +161,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.reduction.alternative": ["true"], "application.fee.reduction.parishCouncil": ["true"], "some.other.fields": ["abc", "xyz"], @@ -163,7 +179,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.reduction.alternative": ["false"], "application.fee.reduction.parishCouncil": ["false"], "some.other.fields": ["abc", "xyz"], @@ -178,7 +194,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.reduction.alternative": ["true"], "application.fee.reduction.parishCouncil": ["false"], "application.fee.reduction.someReason": ["true"], @@ -200,7 +216,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.exemption.disability": ["true"], "application.fee.exemption.resubmission": ["true"], "some.other.fields": ["abc", "xyz"], @@ -218,7 +234,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.exemption.disability": ["false"], "application.fee.exemption.resubmission": ["false"], "some.other.fields": ["abc", "xyz"], @@ -233,7 +249,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": 1000, "application.fee.payable": 800, - "application.fee.payable.vat": 160, + "application.fee.payable.includesVAT": ["true"], "application.fee.exemption.disability": ["false"], "application.fee.exemption.resubmission": ["false"], "application.fee.exemption.someReason": ["true"], @@ -264,7 +280,7 @@ describe("getFeeBreakdown() function", () => { it("returns undefined for partial data", () => { const mockPassportData = { "application.fee.calculated": [1000], - "application.fee.payable.vat": [160], + "application.fee.payable.includesVAT": ["true"], "some.other.fields": ["abc", "xyz"], }; @@ -275,7 +291,7 @@ describe("getFeeBreakdown() function", () => { const mockPassportData = { "application.fee.calculated": "some string", "application.fee.payable": [800, 700], - "application.fee.payable.vat": false, + "application.fee.payable.includesVAT": false, "some.other.fields": ["abc", "xyz"], }; diff --git a/src/utils/feeBreakdown.ts b/src/utils/feeBreakdown.ts index 4602dbe2..93e69778 100644 --- a/src/utils/feeBreakdown.ts +++ b/src/utils/feeBreakdown.ts @@ -2,6 +2,8 @@ import { z } from "zod"; import { FeeBreakdown, PassportFeeFields } from "../types"; +export const VAT_RATE = 0.2; + export const toNumber = (input: number | [number]) => Array.isArray(input) ? input[0] : input; @@ -28,6 +30,9 @@ const getGranularKeys = ( return granularKeys; }; +const getCalculatedAmount = (data: PassportFeeFields) => + data["application.fee.calculated"] || data["application.fee.payable"]; + /** * A "reduction" is the sum of the difference between calculated and payable */ @@ -36,15 +41,24 @@ export const calculateReduction = (data: PassportFeeFields) => ? data["application.fee.calculated"] - data["application.fee.payable"] : 0; +const calculateVAT = (data: PassportFeeFields) => { + if (!data["application.fee.payable.includesVAT"]) return 0; + + const calculated = getCalculatedAmount(data); + const vat = (calculated * VAT_RATE) / (1 + VAT_RATE); + const roundedVAT = Number(vat.toFixed(2)); + + return roundedVAT; +}; + /** * Transform Passport data to a FeeBreakdown */ export const toFeeBreakdown = (data: PassportFeeFields): FeeBreakdown => ({ amount: { - calculated: - data["application.fee.calculated"] || data["application.fee.payable"], + calculated: getCalculatedAmount(data), payable: data["application.fee.payable"], - vat: data["application.fee.payable.vat"], + vat: calculateVAT(data), reduction: calculateReduction(data), }, reductions: getGranularKeys(data, "application.fee.reduction"), @@ -68,7 +82,7 @@ export const createPassportSchema = () => { .object({ "application.fee.calculated": feeSchema.optional().default(0), "application.fee.payable": feeSchema, - "application.fee.payable.vat": feeSchema.optional().default(0), + "application.fee.payable.includesVAT": booleanSchema, "application.fee.reduction.alternative": booleanSchema, "application.fee.reduction.parishCouncil": booleanSchema, "application.fee.reduction.sports": booleanSchema, diff --git a/src/utils/index.ts b/src/utils/index.ts index 2e090575..1f736b0b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ export * from "./digitalPlanningSchema"; export * from "./encryption"; -export { getFeeBreakdown } from "./feeBreakdown"; +export { getFeeBreakdown, VAT_RATE } from "./feeBreakdown"; export * from "./govPayMetadata"; export * from "./projectTypes";