Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Initial fee breakdown public UI #4006

Merged
merged 10 commits into from
Nov 27, 2024
23 changes: 7 additions & 16 deletions editor.planx.uk/src/@planx/components/Pay/Editor/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import {
ComponentType as TYPES,
} from "@opensystemslab/planx-core/types";
import {
parsePay,
Pay,
validationSchema,
} from "@planx/components/Pay/model";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { parsePay, Pay, validationSchema } from "@planx/components/Pay/model";
import { Form, Formik } from "formik";
import React from "react";
import { ComponentTagSelect } from "ui/editor/ComponentTagSelect";
Expand All @@ -20,13 +14,13 @@ import { Switch } from "ui/shared/Switch";

import { ICONS } from "../../shared/icons";
import { EditorProps } from "../../shared/types";
import { FeeBreakdownSection } from "./FeeBreakdownSection";
import { GovPayMetadataSection } from "./GovPayMetadataSection";
import { InviteToPaySection } from "./InviteToPaySection";

export type Props = EditorProps<TYPES.Pay, Pay>;

const Component: React.FC<Props> = (props: Props) => {

const onSubmit = (newValues: Pay) => {
if (props.handleSubmit) {
props.handleSubmit({ type: TYPES.Pay, data: newValues });
Expand All @@ -41,11 +35,7 @@ const Component: React.FC<Props> = (props: Props) => {
validateOnChange={true}
validateOnBlur={true}
>
{({
values,
handleChange,
setFieldValue,
}) => (
{({ values, handleChange, setFieldValue }) => (
<Form id="modal" name="modal">
<ModalSection>
<ModalSectionContent title="Payment" Icon={ICONS[TYPES.Pay]}>
Expand Down Expand Up @@ -112,8 +102,9 @@ const Component: React.FC<Props> = (props: Props) => {
</InputRow>
</ModalSectionContent>
</ModalSection>
<GovPayMetadataSection/>
<InviteToPaySection/>
<GovPayMetadataSection />
<InviteToPaySection />
<FeeBreakdownSection />
<MoreInformation
changeField={handleChange}
definitionImg={values.definitionImg}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
import { useFormikContext } from "formik";
import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";

import { Pay } from "../model";

export const FeeBreakdownSection: React.FC = () => {
const { values, setFieldValue } = useFormikContext<Pay>();

if (!hasFeatureFlag("FEE_BREAKDOWN")) return null;

return (
<ModalSection>
<ModalSectionContent title="Fee breakdown" Icon={ReceiptLongIcon}>
<InputRow>
<Switch
checked={values.showFeeBreakdown}
onChange={() =>
setFieldValue("showFeeBreakdown", !values.showFeeBreakdown)
}
label="Display a breakdown of the fee to the applicant"
/>
</InputRow>
</ModalSectionContent>
</ModalSection>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from "react";
import Box from "@mui/material/Box";
import {
ComponentType as TYPES,
} from "@opensystemslab/planx-core/types";
import { ComponentType as TYPES } from "@opensystemslab/planx-core/types";
import { useFormikContext } from "formik";
import React from "react";
import ModalSection from "ui/editor/ModalSection";
import ModalSectionContent from "ui/editor/ModalSectionContent";
import RichTextInput from "ui/editor/RichTextInput/RichTextInput";
Expand All @@ -11,7 +10,6 @@ import InputRow from "ui/shared/InputRow";
import { Switch } from "ui/shared/Switch";

import { ICONS } from "../../shared/icons";
import { useFormikContext } from "formik";
import { Pay } from "../model";

export const InviteToPaySection: React.FC = () => {
Expand Down Expand Up @@ -98,5 +96,5 @@ export const InviteToPaySection: React.FC = () => {
)}
</ModalSectionContent>
</ModalSection>
)
}
);
};
23 changes: 9 additions & 14 deletions editor.planx.uk/src/@planx/components/Pay/Public/Confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,24 @@ import FormWrapper from "ui/public/FormWrapper";
import ErrorSummary from "ui/shared/ErrorSummary/ErrorSummary";
import ReactMarkdownOrHtml from "ui/shared/ReactMarkdownOrHtml/ReactMarkdownOrHtml";

import { formattedPriceWithCurrencySymbol, getDefaultContent } from "../model";
import {
formattedPriceWithCurrencySymbol,
getDefaultContent,
Pay,
} from "../model";
import { FeeBreakdown } from "./FeeBreakdown";
import InviteToPayForm, { InviteToPayFormProps } from "./InviteToPayForm";
import { PAY_API_ERROR_UNSUPPORTED_TEAM } from "./Pay";

export interface Props {
export interface Props extends Omit<Pay, "title" | "fn" | "govPayMetadata"> {
title?: string;
bannerTitle?: string;
description?: string;
fee: number;
instructionsTitle?: string;
instructionsDescription?: string;
showInviteToPay?: boolean;
secondaryPageTitle?: string;
nomineeTitle?: string;
nomineeDescription?: string;
yourDetailsTitle?: string;
yourDetailsDescription?: string;
yourDetailsLabel?: string;
paymentStatus?: PaymentStatus;
buttonTitle?: string;
onConfirm: () => void;
error?: string;
hideFeeBanner?: boolean;
hidePay?: boolean;
}

interface PayBodyProps extends Props {
Expand Down Expand Up @@ -86,7 +80,7 @@ const PayBody: React.FC<PayBodyProps> = (props) => {
<Card>
<PayText>
<Typography variant="h2" component={props.hideFeeBanner ? "h2" : "h3"}>
{props.instructionsTitle || defaults.instructionsTitle }
{props.instructionsTitle || defaults.instructionsTitle}
</Typography>
<ReactMarkdownOrHtml
source={
Expand Down Expand Up @@ -187,6 +181,7 @@ export default function Confirm(props: Props) {
/>
</Typography>
</FormWrapper>
{props.showFeeBreakdown && <FeeBreakdown />}
</Banner>
)}
{page === "Pay" ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { setup } from "testUtils";
import { vi } from "vitest";
import { axe } from "vitest-axe";

import { FeeBreakdown } from "./FeeBreakdown";

vi.mock("lib/featureFlags", () => ({
hasFeatureFlag: () => true,
}));

it("should not have any accessibility violations", async () => {
const { container } = setup(<FeeBreakdown />);
const results = await axe(container);

expect(results).toHaveNoViolations();
});

// Placeholder tests and initial assumptions
it.todo("displays a planning fee");

it.todo("displays a total");

it.todo("displays a service charge if applicable");
it.todo("does not display a service charge if not applicable");

it.todo("displays VAT if applicable");
it.todo("does not display VAT if not applicable");

it.todo("displays exemptions if applicable");
it.todo("does not exemptions if not applicable");

it.todo("displays reductions if applicable");
it.todo("does not reductions if not applicable");

it.todo("does not display if fee calculation values are invalid");
it.todo("silently throws an error if fee calculations are invalid");
111 changes: 111 additions & 0 deletions editor.planx.uk/src/@planx/components/Pay/Public/FeeBreakdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Box from "@mui/material/Box";
import { styled } from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell, { tableCellClasses } from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Typography from "@mui/material/Typography";
import { hasFeatureFlag } from "lib/featureFlags";
import React from "react";
import { FONT_WEIGHT_SEMI_BOLD } from "theme";

import { formattedPriceWithCurrencySymbol } from "../model";

const StyledTable = styled(Table)(() => ({
[`& .${tableCellClasses.root}`]: {
paddingLeft: 0,
paddingRight: 0,
},
}));

const BoldTableRow = styled(TableRow)(() => ({
[`& .${tableCellClasses.root}`]: {
fontWeight: FONT_WEIGHT_SEMI_BOLD,
},
}));

const VAT_RATE = 20;

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.";

const Header = () => (
<TableHead>
<BoldTableRow>
<TableCell>Description</TableCell>
<TableCell align="right">Amount</TableCell>
</BoldTableRow>
</TableHead>
);

const ApplicationFee = () => (
<TableRow>
<TableCell>Application fee</TableCell>
<TableCell align="right">{formattedPriceWithCurrencySymbol(100)}</TableCell>
</TableRow>
);

const Exemptions = () => (
<TableRow>
<TableCell>Exemptions</TableCell>
<TableCell align="right">{formattedPriceWithCurrencySymbol(-20)}</TableCell>
</TableRow>
);

const Reductions = () => (
<TableRow>
<TableCell>Reductions</TableCell>
<TableCell align="right">{formattedPriceWithCurrencySymbol(-30)}</TableCell>
</TableRow>
);

const ServiceCharge = () => (
<TableRow>
<TableCell>Service charge</TableCell>
<TableCell align="right">{formattedPriceWithCurrencySymbol(30)}</TableCell>
</TableRow>
);

const VAT = () => (
<TableRow>
<TableCell>{`VAT (${VAT_RATE}%)`}</TableCell>
<TableCell align="right">-</TableCell>
</TableRow>
);

const Total = () => (
<BoldTableRow>
<TableCell>Total</TableCell>
<TableCell align="right">{formattedPriceWithCurrencySymbol(80)}</TableCell>
</BoldTableRow>
);

export const FeeBreakdown: React.FC = () => {
if (!hasFeatureFlag("FEE_BREAKDOWN")) return null;

return (
<Box mt={3}>
<Typography variant="h3" mb={1}>
Fee breakdown
</Typography>
<Typography variant="body1" mb={2}>
{DESCRIPTION}
</Typography>
<TableContainer>
<StyledTable data-testid="fee-breakdown-table">
<Header />
<TableBody>
<ApplicationFee />
<ServiceCharge />
<Exemptions />
<Reductions />
<VAT />
<Total />
</TableBody>
</StyledTable>
</TableContainer>
</Box>
);
};
20 changes: 20 additions & 0 deletions editor.planx.uk/src/@planx/components/Pay/Public/Pay.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const meta = {
}),
},
},
loaders: [
() => window.localStorage.setItem("FEATURE_FLAGS", '["FEE_BREAKDOWN"]'),
],
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooh didn't know you could do this! 💡

} satisfies Meta<typeof Confirm>;

type Story = StoryObj<typeof meta>;
Expand Down Expand Up @@ -71,3 +74,20 @@ export const ForInformationOnly = {
onConfirm: () => {},
},
} satisfies Story;

// TODO: Setup fee breakdown amounts
export const WithFeeBreakdown = {
args: {
title: "Pay for your application",
bannerTitle: "The fee is",
description: "The fee covers the cost of processing your application",
fee: 103,
instructionsTitle: "How to pay",
instructionsDescription: "Pay via GOV.UK Pay",
buttonTitle: "Pay",
onConfirm: () => {},
error: undefined,
showInviteToPay: false,
showFeeBreakdown: true,
},
} satisfies Story;
Loading
Loading