Skip to content

Commit

Permalink
feat(fee-breakdown): Parse and display discrete list of reductions an…
Browse files Browse the repository at this point in the history
…d exemptions (#4040)
  • Loading branch information
DafyddLlyr authored Dec 5, 2024
1 parent 87d5d44 commit 6f7f8b6
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 39 deletions.
6 changes: 3 additions & 3 deletions editor.planx.uk/src/@planx/components/Pay/Editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ const Component: React.FC<Props> = (props: Props) => {
</InputRow>
</ModalSectionContent>
</ModalSection>
<GovPayMetadataSection />
<InviteToPaySection />
<FeeBreakdownSection />
<GovPayMetadataSection/>
<InviteToPaySection/>
<FeeBreakdownSection/>
<MoreInformation
changeField={handleChange}
definitionImg={values.definitionImg}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import { hasFeatureFlag } from 'lib/featureFlags';
import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder";
import ModalSection from "ui/editor/ModalSection";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,52 @@ const ApplicationFee: React.FC<{ amount: number }> = ({ amount }) => (
</TableRow>
);

const Reductions: React.FC<{ amount?: number }> = ({ amount }) => {
const Reductions: React.FC<{ amount?: number, reductions: string[] }> = ({ amount, reductions }) => {
if (!amount) return null;

return (
<>
<TableRow>
<TableCell>Reductions</TableCell>
<TableCell align="right">
{formattedPriceWithCurrencySymbol(-amount)}
</TableCell>
</TableRow>
{
reductions.map((reduction) => (
<TableRow>
<TableCell colSpan={2}>
<Box sx={{ pl: 2, color: "grey" }}>{reduction}</Box>
</TableCell>
</TableRow>
))
}
</>
);
};

// TODO: This won't show as if a fee is 0, we hide the whole Pay component from the user
const Exemptions: React.FC<{ amount: number, exemptions: string[] }> = ({ amount, exemptions }) => {
if (!exemptions.length) return null;

return (
<>
<TableRow>
<TableCell>Exemptions</TableCell>
<TableCell align="right">
{formattedPriceWithCurrencySymbol(-amount)}
</TableCell>
</TableRow>
{
exemptions.map((exemption) => (
<TableRow>
<TableCell colSpan={2}>
<Box sx={{ pl: 2, color: "grey" }}>{exemption}</Box>
</TableCell>
</TableRow>
))
}
</>
);
};

Expand Down Expand Up @@ -90,6 +126,8 @@ export const FeeBreakdown: React.FC = () => {
const breakdown = useFeeBreakdown();
if (!breakdown) return null;

const { amount, reductions, exemptions } = breakdown;

return (
<Box mt={3}>
<Typography variant="h3" mb={1}>
Expand All @@ -102,10 +140,11 @@ export const FeeBreakdown: React.FC = () => {
<StyledTable data-testid="fee-breakdown-table">
<Header />
<TableBody>
<ApplicationFee amount={breakdown.applicationFee} />
<Reductions amount={breakdown.reduction} />
<Total amount={breakdown.total} />
<VAT amount={breakdown.vat} />
<ApplicationFee amount={amount.applicationFee} />
<Reductions amount={amount.reduction} reductions={reductions}/>
<Exemptions amount={amount.total} exemptions={exemptions}/>
<Total amount={amount.total} />
<VAT amount={amount.vat} />
</TableBody>
</StyledTable>
</TableContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
export interface FeeBreakdown {
applicationFee: number;
total: number;
reduction: number;
vat: number | undefined;
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;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ describe("useFeeBreakdown() hook", () => {
"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({
applicationFee: 1000,
total: 800,
reduction: 200,
vat: 160,
amount: {
applicationFee: 1000,
total: 800,
reduction: 200,
vat: 160,
},
exemptions: [],
reductions: [],
});
});

Expand All @@ -40,24 +45,155 @@ describe("useFeeBreakdown() hook", () => {
"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({
applicationFee: 1000,
total: 800,
reduction: 200,
vat: 160,
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 = {};
const mockPassportData = {
"some.other.fields": ["abc", "xyz"],
};

vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]);

Expand All @@ -70,6 +206,7 @@ describe("useFeeBreakdown() hook", () => {
const mockPassportData = {
"application.fee.calculated": [1000],
"application.fee.payable.vat": [160],
"some.other.fields": ["abc", "xyz"],
};

vi.mocked(useStore).mockReturnValue([mockPassportData, "test-session"]);
Expand All @@ -84,6 +221,7 @@ describe("useFeeBreakdown() hook", () => {
"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"]);
Expand All @@ -94,7 +232,9 @@ describe("useFeeBreakdown() hook", () => {
});

it("calls Airbrake if invalid inputs are provided", () => {
const mockPassportData = {};
const mockPassportData = {
"some.other.fields": ["abc", "xyz"],
};
const mockSessionId = "test-session";

vi.mocked(useStore).mockReturnValue([mockPassportData, mockSessionId]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const useFeeBreakdown = (): FeeBreakdown | undefined => {
state.computePassport().data,
state.sessionId,
]);
if (!passportData) return

const schema = createPassportSchema();
const result = schema.safeParse(passportData);

Expand Down
Loading

0 comments on commit 6f7f8b6

Please sign in to comment.