From 08d74401ff57e95e0183a8807f0126e3b9ef4183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Thu, 5 Dec 2024 12:16:38 +0000 Subject: [PATCH] feat: Import fee breakdown logic from planx-core --- .../Pay/Public/FeeBreakdown/types.ts | 21 -- .../FeeBreakdown/useFeeBreakdown.test.ts | 256 ------------------ .../Public/FeeBreakdown/useFeeBreakdown.tsx | 19 +- .../Pay/Public/FeeBreakdown/utils.test.ts | 91 ------- .../Pay/Public/FeeBreakdown/utils.ts | 83 ------ 5 files changed, 8 insertions(+), 462 deletions(-) delete mode 100644 editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.ts delete mode 100644 editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts delete mode 100644 editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts delete mode 100644 editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts 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 deleted file mode 100644 index 176cf87724..0000000000 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface FeeBreakdown { - amount: { - applicationFee: number; - total: number; - reduction: number; - vat: number | undefined; - }; - reductions: string[]; - exemptions: string[]; -} - -export interface PassportFeeFields { - "application.fee.calculated": number; - "application.fee.payable": number; - "application.fee.payable.vat": number; - "application.fee.reduction.alternative": boolean; - "application.fee.reduction.parishCouncil": boolean; - "application.fee.reduction.sports": boolean; - "application.fee.exemption.disability": boolean; - "application.fee.exemption.resubmission": boolean; -}; \ No newline at end of file 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 deleted file mode 100644 index 5312cb12c2..0000000000 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import { logger } from "airbrake"; -import { useStore } from "pages/FlowEditor/lib/store"; -import { vi } from "vitest"; - -import { useFeeBreakdown } from "./useFeeBreakdown"; - -vi.mock("pages/FlowEditor/lib/store", () => ({ - useStore: vi.fn(), -})); - -vi.mock("airbrake", () => ({ - logger: { - notify: vi.fn(), - }, -})); - -describe("useFeeBreakdown() hook", () => { - describe("valid data", () => { - it("returns a fee breakdown for number inputs", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result).toEqual({ - amount: { - applicationFee: 1000, - total: 800, - reduction: 200, - vat: 160, - }, - exemptions: [], - reductions: [], - }); - }); - - it("returns a fee breakdown for number tuple inputs", () => { - const mockPassportData = { - "application.fee.calculated": [1000], - "application.fee.payable": [800], - "application.fee.payable.vat": [160], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result).toEqual({ - amount: { - applicationFee: 1000, - total: 800, - reduction: 200, - vat: 160, - }, - exemptions: [], - reductions: [], - }); - }); - - it("parses 'true' reduction values to a list of keys", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.reduction.alternative": ["true"], - "application.fee.reduction.parishCouncil": ["true"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result?.reductions).toHaveLength(2); - expect(result?.reductions).toEqual( - expect.arrayContaining(["alternative", "parishCouncil"]) - ); - }); - - it("does not parse 'false' reduction values to a list of keys", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.reduction.alternative": ["false"], - "application.fee.reduction.parishCouncil": ["false"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result?.reductions).toHaveLength(0); - }); - - it("does not parse non-schema reduction values", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.reduction.alternative": ["true"], - "application.fee.reduction.parishCouncil": ["false"], - "application.fee.reduction.someReason": ["true"], - "application.fee.reduction.someOtherReason": ["false"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([ - mockPassportData, - "test-session", - ]); - - const result = useFeeBreakdown(); - - expect(result?.reductions).toEqual(expect.not.arrayContaining(["someReason"])) - expect(result?.reductions).toEqual(expect.not.arrayContaining(["someOtherReason"])) - }); - - it("parses 'true' exemption values to a list of keys", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.exemption.disability": ["true"], - "application.fee.exemption.resubmission": ["true"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result?.exemptions).toHaveLength(2); - expect(result?.exemptions).toEqual( - expect.arrayContaining(["disability", "resubmission"]) - ); - }); - - it("does not parse 'false' exemption values to a list of keys", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.exemption.disability": ["false"], - "application.fee.exemption.resubmission": ["false"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result?.exemptions).toHaveLength(0); - }); - - it("does not parse non-schema exemption values", () => { - const mockPassportData = { - "application.fee.calculated": 1000, - "application.fee.payable": 800, - "application.fee.payable.vat": 160, - "application.fee.exemption.disability": ["false"], - "application.fee.exemption.resubmission": ["false"], - "application.fee.exemption.someReason": ["true"], - "application.fee.exemption.someOtherReason": ["false"], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([ - mockPassportData, - "test-session", - ]); - - const result = useFeeBreakdown(); - - expect(result?.exemptions).toEqual( - expect.not.arrayContaining(["someReason"]) - ); - expect(result?.exemptions).toEqual( - expect.not.arrayContaining(["someOtherReason"]) - ); - }); - }); - - describe("invalid inputs", () => { - it("returns undefined for missing data", () => { - const mockPassportData = { - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result).toBeUndefined(); - }); - - it("returns undefined for partial data", () => { - const mockPassportData = { - "application.fee.calculated": [1000], - "application.fee.payable.vat": [160], - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result).toBeUndefined(); - }); - - it("returns undefined for incorrect data", () => { - const mockPassportData = { - "application.fee.calculated": "some string", - "application.fee.payable": [800, 700], - "application.fee.payable.vat": false, - "some.other.fields": ["abc", "xyz"], - }; - - vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]); - - const result = useFeeBreakdown(); - - expect(result).toBeUndefined(); - }); - - it("calls Airbrake if invalid inputs are provided", () => { - const mockPassportData = { - "some.other.fields": ["abc", "xyz"], - }; - const mockSessionId = "test-session"; - - vi.mocked(useStore).mockReturnValue([mockPassportData, mockSessionId]); - const loggerSpy = vi.spyOn(logger, "notify"); - - const result = useFeeBreakdown(); - - expect(result).toBeUndefined(); - - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining(mockSessionId), - ); - - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining("ZodError"), - ); - }); - }); -}); diff --git a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.tsx b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.tsx index b1e6b10c1d..0a296ec3bf 100644 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.tsx +++ b/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/useFeeBreakdown.tsx @@ -1,9 +1,8 @@ +import { getFeeBreakdown } from "@opensystemslab/planx-core"; +import { FeeBreakdown } from "@opensystemslab/planx-core/types"; import { logger } from "airbrake"; import { useStore } from "pages/FlowEditor/lib/store"; -import { FeeBreakdown } from "./types"; -import { createPassportSchema } from "./utils"; - /** * Parses the users's Passport for data variables associated with their fee * Currently relies on static `application.fee.x` variables @@ -16,17 +15,15 @@ export const useFeeBreakdown = (): FeeBreakdown | undefined => { state.computePassport().data, state.sessionId, ]); - if (!passportData) return - - const schema = createPassportSchema(); - const result = schema.safeParse(passportData); + if (!passportData) return; - if (!result.success) { + try { + const feeBreakdown = getFeeBreakdown(passportData); + return feeBreakdown; + } catch (error) { logger.notify( - `Failed to parse fee breakdown data from passport for session ${sessionId}. Error: ${result.error}`, + `Failed to parse fee breakdown data from passport for session ${sessionId}. Error: ${error}`, ); return; } - - return result.data; }; 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 deleted file mode 100644 index 935c8cb066..0000000000 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { PassportFeeFields } from "./types"; -import { calculateReduction, toFeeBreakdown, toNumber } from "./utils"; - -describe("toNumber() helper function", () => { - it("outputs a number when passed a number", () => { - const input = 12; - const output = toNumber(input); - - expect(output).toEqual(input); - }); - - it("outputs a number when passed a number tuple", () => { - const input: [number] = [12]; - const output = toNumber(input); - - expect(output).toEqual(12); - }); -}); - -describe("calculateReduction() helper function", () => { - it("correctly outputs the reduction when a calculated value is provided", () => { - const input: PassportFeeFields = { - "application.fee.calculated": 100, - "application.fee.payable": 50, - "application.fee.payable.vat": 0, - "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 reduction = calculateReduction(input); - - expect(reduction).toEqual(50); - }); - - it("defaults to 0 when calculated is 0", () => { - const input: PassportFeeFields = { - "application.fee.calculated": 0, - "application.fee.payable": 100, - "application.fee.payable.vat": 0, - "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 reduction = calculateReduction(input); - - expect(reduction).toEqual(0); - }); -}); - -describe("toFeeBreakdown() helper function", () => { - it("correctly maps fields", () => { - const input: PassportFeeFields = { - "application.fee.calculated": 100, - "application.fee.payable": 50, - "application.fee.payable.vat": 10, - "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.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": 50, - "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.applicationFee).toEqual(input["application.fee.payable"]); - }); -}); 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 deleted file mode 100644 index 759c5438db..0000000000 --- a/editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown/utils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { z } from "zod"; - -import { FeeBreakdown, PassportFeeFields } from "./types"; - -export const toNumber = (input: number | [number]) => - Array.isArray(input) ? input[0] : input; - -/** - * Convert a Passport value to an actual boolean - */ -const toBoolean = (val: ["true" | "false"]) => val[0] === "true"; - - -/** - * Iterate over exemptions or reductions to find matches, returning the granular keys - */ - const getGranularKeys = ( - data: PassportFeeFields, - prefix: "application.fee.reduction" | "application.fee.exemption" - ) => { - const keys = Object.keys(data) as (keyof PassportFeeFields)[]; - const intersectingKeys = keys.filter( - (key) => key.startsWith(prefix) && Boolean(data[key]) - ); - const granularKeys = intersectingKeys.map((key) => - key.replace(prefix + ".", "") - ); - - return granularKeys; - }; - -/** - * A "reduction" is the sum of the difference between calculated and payable - */ -export const calculateReduction = (data: PassportFeeFields) => - data["application.fee.calculated"] - ? data["application.fee.calculated"] - data["application.fee.payable"] - : 0; - -/** - * Transform Passport data to a FeeBreakdown - */ -export const toFeeBreakdown = (data: PassportFeeFields): FeeBreakdown => ({ - amount: { - applicationFee: - data["application.fee.calculated"] || - data["application.fee.payable"], - total: data["application.fee.payable"], - vat: data["application.fee.payable.vat"], - reduction: calculateReduction(data), - }, - reductions: getGranularKeys(data, "application.fee.reduction"), - exemptions: getGranularKeys(data, "application.fee.exemption"), -}); - -export const createPassportSchema = () => { - const questionSchema = z.number().nonnegative(); - const setValueSchema = z.tuple([z.coerce.number().nonnegative()]); - const feeSchema = z - .union([questionSchema, setValueSchema]) - .transform(toNumber); - - /** Describes how boolean values are set via PlanX components */ - const booleanSchema = z - .tuple([z.enum(["true", "false"])]) - .default(["false"]) - .transform(toBoolean) - - const schema = z - .object({ - "application.fee.calculated": feeSchema.optional().default(0), - "application.fee.payable": feeSchema, - "application.fee.payable.vat": feeSchema.optional().default(0), - "application.fee.reduction.alternative": booleanSchema, - "application.fee.reduction.parishCouncil": booleanSchema, - "application.fee.reduction.sports": booleanSchema, - "application.fee.exemption.disability": booleanSchema, - "application.fee.exemption.resubmission": booleanSchema, - }) - .transform(toFeeBreakdown); - - return schema; -};