From c926c05550c2d6252a54a37c68c3763ca8801b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 5 Dec 2024 08:34:20 +0000 Subject: [PATCH] feat: Calculate VAT using `useFeeBreakdown()`, not via `Calculate` component --- .../Pay/Public/FeeBreakdown/FeeBreakdown.tsx | 2 +- .../Pay/Public/FeeBreakdown/types.ts | 2 +- .../FeeBreakdown/useFeeBreakdown.test.ts | 28 +++++++++---------- .../Pay/Public/FeeBreakdown/utils.test.ts | 26 +++++++++++++---- .../Pay/Public/FeeBreakdown/utils.ts | 22 +++++++++++---- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/FeeBreakdown.tsx b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/FeeBreakdown.tsx index 2fcccf850b..8e600dbc21 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/FeeBreakdown.tsx +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/FeeBreakdown.tsx @@ -26,7 +26,7 @@ const BoldTableRow = styled(TableRow)(() => ({ }, })); -const VAT_RATE = 0.2; +export const VAT_RATE = 0.2; const DESCRIPTION = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."; diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.ts b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.ts index 176cf87724..d2015afec9 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.ts +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.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/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts index 6f4158028c..19e2eb2729 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts @@ -20,7 +20,7 @@ describe("useFeeBreakdown() hook", () => { 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"], }; @@ -33,7 +33,7 @@ describe("useFeeBreakdown() hook", () => { applicationFee: 1000, total: 800, reduction: 200, - vat: 160, + vat: 166.67, }, exemptions: [], reductions: [], @@ -44,7 +44,7 @@ describe("useFeeBreakdown() hook", () => { 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"], }; @@ -57,7 +57,7 @@ describe("useFeeBreakdown() hook", () => { applicationFee: 1000, total: 800, reduction: 200, - vat: 160, + vat: 166.67, }, exemptions: [], reductions: [], @@ -68,7 +68,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -88,7 +88,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -105,7 +105,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -128,7 +128,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -148,7 +148,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -165,7 +165,7 @@ describe("useFeeBreakdown() hook", () => { 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"], @@ -180,10 +180,10 @@ describe("useFeeBreakdown() hook", () => { const result = useFeeBreakdown(); - expect(result?.exemption).toEqual( + expect(result?.exemptions).toEqual( expect.not.arrayContaining(["someReason"]) ); - expect(result?.exemption).toEqual( + expect(result?.exemptions).toEqual( expect.not.arrayContaining(["someOtherReason"]) ); }); @@ -205,7 +205,7 @@ describe("useFeeBreakdown() hook", () => { 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"], }; @@ -220,7 +220,7 @@ describe("useFeeBreakdown() hook", () => { 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/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts index 935c8cb066..ec778875eb 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts @@ -22,7 +22,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, @@ -38,7 +38,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, @@ -56,7 +56,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, @@ -68,14 +68,13 @@ describe("toFeeBreakdown() helper function", () => { expect(amount.applicationFee).toEqual(input["application.fee.calculated"]); expect(amount.total).toEqual(input["application.fee.payable"]); - expect(amount.vat).toEqual(input["application.fee.payable.vat"]); expect(amount.reduction).toEqual(50); }); it("sets applicationFee 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, @@ -88,4 +87,21 @@ describe("toFeeBreakdown() helper function", () => { expect(amount.applicationFee).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); + }); }); diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts index 759c5438db..6ee8055a9f 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { VAT_RATE } from "./FeeBreakdown"; import { FeeBreakdown, PassportFeeFields } from "./types"; export const toNumber = (input: number | [number]) => @@ -29,6 +30,9 @@ const toBoolean = (val: ["true" | "false"]) => val[0] === "true"; return granularKeys; }; +export const calculateApplicationFee = (data: PassportFeeFields) => + data["application.fee.calculated"] || data["application.fee.payable"] + /** * A "reduction" is the sum of the difference between calculated and payable */ @@ -37,16 +41,24 @@ export const calculateReduction = (data: PassportFeeFields) => ? data["application.fee.calculated"] - data["application.fee.payable"] : 0; +export const calculateVAT = (data: PassportFeeFields) => { + if (!data["application.fee.payable.includesVAT"]) return 0; + + const fee = calculateApplicationFee(data); + const vat = (fee * 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: { - applicationFee: - data["application.fee.calculated"] || - data["application.fee.payable"], + applicationFee: calculateApplicationFee(data), total: data["application.fee.payable"], - vat: data["application.fee.payable.vat"], + vat: calculateVAT(data), reduction: calculateReduction(data), }, reductions: getGranularKeys(data, "application.fee.reduction"), @@ -70,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,