Skip to content

Commit

Permalink
feat: add pay warning page (#1164)
Browse files Browse the repository at this point in the history
  • Loading branch information
jenbutongit authored Dec 1, 2023
1 parent 0b3c792 commit 9f0bcaf
Show file tree
Hide file tree
Showing 14 changed files with 280 additions and 80 deletions.
88 changes: 58 additions & 30 deletions docs/runner/fee-options.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
# Fee options
# Fee options and Payment skipped warning page

`feeOptions` is a top level property in a form json. Fee options are used to configure API keys for GOV.UK Pay, and the behaviour of retrying payments.
## Fee options

`feeOptions` is a top level property in a form json. Fee options are used to configure API keys for GOV.UK Pay, and the behaviour of retrying payments.

```.json5
```json5
{
// pages, sections, conditions etc ..
"feeOptions": {
/**
* If a payment is required, but the user fails, allow the user to skip payment
* and submit the form. this is the default behaviour.
*
* Any versions AFTER (and not including) v3.25.68-rc.927 allows this behaviour
* to be configurable. If you do not want payment to be skippable, set
* `allowSubmissionWithoutPayment: false`
*/
"allowSubmissionWithoutPayment": true,

/**
* The maximum number of times a user can attempt to pay before the form is auto submitted.
* There is no limit when allowSubmissionWithoutPayment is false. (The user can retry as many times as they like).
*/
"maxAttempts": 3,

/**
* A supplementary error message (`customPayErrorMessage`)
*/
"customPayErrorMessage": "Custom error message",
}
feeOptions: {
/**
* If a payment is required, but the user fails, allow the user to skip payment
* and submit the form. this is the default behaviour.
*
* Any versions AFTER (and not including) v3.25.68-rc.927 allows this behaviour
* to be configurable. If you do not want payment to be skippable, set
* `allowSubmissionWithoutPayment: false`
*/
allowSubmissionWithoutPayment: true,

/**
* The maximum number of times a user can attempt to pay before the form is auto submitted.
* There is no limit when allowSubmissionWithoutPayment is false. (The user can retry as many times as they like).
*/
maxAttempts: 3,

/**
* A supplementary error message (`customPayErrorMessage`)
*/
customPayErrorMessage: "Custom error message",

/**
* Shows a link (button) below the "Submit and pay" button on the summary page. Clicking this will take the user to a page
* that provides additional messaging, you can warn the user that this may delay their application for example.
* allowSubmissionWithoutPayment must be true for this to be shown.
*/
showPaymentSkippedWarningPage: false,
},
}
```

Expand All @@ -36,12 +44,32 @@ This is the default behaviour. Makes sure you check your organisations policy or

When a user fails a payment, they will see the page [pay-error](./../../runner/src/server/views/pay-error.html).

When `allowSubmissionWithoutPayment` is true, the user will also see a link which allows them to skip payment.
When `allowSubmissionWithoutPayment` is true, the user will also see a link which allows them to skip payment.

### Recommendations

## Recommendations

If your service does not allow submission without payment, set
If your service does not allow submission without payment, set
`allowSubmissionWithoutPayment: false`. `maxAttempts` will have no effect. The user will be able to retry as many times as they like.
You can provide them with `customPayErrorMessage` to provide them with another route to payment.
You can provide them with `customPayErrorMessage` to provide them with another route to payment.

## paymentSkippedWarningPage

`paymentSkippedWarningPage` can be found on the `specialPages` top level property.

If `feeOptions.showPaymentSkippedWarningPage` (and `feeOptions.allowSubmissionWithoutPayment`) is true,
another page ([payment-skip-warning](./../../runner/src/server/views/payment-skip-warning.html)) will be presented to the user.
Additional messaging can be provided to the user for alternative routes to payment, or may result in application delays.
The can choose to continue or try online payment. This page will be shown only once.

```json5
{
// pages, sections, conditions etc ..
paymentSkippedWarningPage: {
customText: {
caption: "Payment",
title: "Pay at appointment",
body: '<p class="govuk-body">You have chosen to skip payment. You will not be able to submit your application until you have paid.</p><p class="govuk-body">If you are unable to pay online, you\'ll need to bring the equivalent of £50 in cash in the local currency to your appointment. You wil not be given any change. <a href="">Check current consular exchange rates</a></p>',
},
},
}
```
26 changes: 19 additions & 7 deletions model/src/data-model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,17 @@ export type ConfirmationPage = {
components: ComponentDef[];
};

export type PaymentSkippedWarningPage = {
customText: {
title: string;
caption: string;
body: string;
};
};

export type SpecialPages = {
confirmationPage?: ConfirmationPage;
paymentSkippedWarningPage?: PaymentSkippedWarningPage;
};

export function isMultipleApiKey(
Expand All @@ -136,6 +145,15 @@ export type Fee = {
prefix?: string;
};

export type FeeOptions = {
paymentReferenceFormat?: string;
payReturnUrl?: string;
allowSubmissionWithoutPayment: boolean;
maxAttempts: number;
customPayErrorMessage?: string;
showPaymentSkippedWarningPage: boolean;
};

/**
* `FormDefinition` is a typescript representation of `Schema`
*/
Expand All @@ -156,11 +174,5 @@ export type FormDefinition = {
payApiKey?: string | MultipleApiKeys | undefined;
specialPages?: SpecialPages;
paymentReferenceFormat?: string;
feeOptions: {
paymentReferenceFormat?: string;
payReturnUrl?: string;
allowSubmissionWithoutPayment: boolean;
maxAttempts: number;
customPayErrorMessage?: string;
};
feeOptions: FeeOptions;
};
2 changes: 2 additions & 0 deletions model/src/schema/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe("payment configuration", () => {
maxAttempts: 10,
paymentReferenceFormat: "EGGS-",
payReturnUrl: "https://my.egg.service.scramble",
showPaymentSkippedWarningPage: false,
});
});

Expand All @@ -111,6 +112,7 @@ describe("payment configuration", () => {
maxAttempts: 3,
paymentReferenceFormat: "EGGS-",
payReturnUrl: "https://my.egg.service.scramble",
showPaymentSkippedWarningPage: false,
});
});
});
16 changes: 15 additions & 1 deletion model/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,17 @@ const confirmationPageSchema = joi.object({
components: joi.array().items(componentSchema),
});

const paymentSkippedWarningPage = joi.object({
customText: joi.object({
title: joi.string().default("Pay for your application").optional(),
caption: joi.string().default("Payment").optional(),
body: joi.string().default("").optional(),
}),
});

const specialPagesSchema = joi.object().keys({
confirmationPage: confirmationPageSchema,
confirmationPage: confirmationPageSchema.optional(),
paymentSkippedWarningPage: paymentSkippedWarningPage.optional(),
});

const listItemSchema = joi.object().keys({
Expand Down Expand Up @@ -239,6 +248,11 @@ const feeOptionSchema = joi
allowSubmissionWithoutPayment: joi.boolean().optional().default(true),
maxAttempts: joi.number().optional().default(3),
customPayErrorMessage: joi.string().optional(),
showPaymentSkippedWarningPage: joi.when("allowSubmissionWithoutPayment", {
is: true,
then: joi.boolean().valid(true, false).default(false),
otherwise: joi.boolean().valid(false).default(false),
}),
})
.default(({ payApiKey, paymentReferenceFormat }) => {
return {
Expand Down
15 changes: 13 additions & 2 deletions runner/src/client/sass/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
@import "modal-dialog";
@import "upload-dialog";


.flash-card {
.govuk-button {
background: #fff;
Expand Down Expand Up @@ -66,11 +65,23 @@
&--hidden-titles {
@extend .govuk-visually-hidden;
}

}
&__key {
&--hidden-titles {
width: 100%;
}
}
}

.govuk-button {
&--link {
@extend .govuk-link;
border: none;
color: $govuk-link-colour;
cursor: pointer;
background-color: transparent;
&:hover {
color: $govuk-link-hover-colour;
}
}
}
61 changes: 47 additions & 14 deletions runner/src/server/plugins/applicationStatus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@ import { HapiRequest, HapiResponseToolkit } from "../../types";
import { retryPay } from "./retryPay";
import { handleUserWithConfirmationViewModel } from "./handleUserWithConfirmationViewModel";
import { checkUserCompletedSummary } from "./checkUserCompletedSummary";
import config from "server/config";

import Joi from "joi";
import {
continueToPayAfterPaymentSkippedWarning,
paymentSkippedWarning,
} from "./paymentSkippedWarning";

const preHandlers = {
retryPay: {
method: retryPay,
assign: "shouldShowPayErrorPage",
},
handleUserWithConfirmationViewModel: {
method: handleUserWithConfirmationViewModel,
assign: "confirmationViewModel",
},
checkUserCompletedSummary: {
method: checkUserCompletedSummary,
assign: "userCompletedSummary",
},
};

const index = {
plugin: {
Expand All @@ -16,22 +36,12 @@ const index = {
path: "/{id}/status",
options: {
pre: [
{
method: retryPay,
assign: "shouldShowPayErrorPage",
},
{
method: handleUserWithConfirmationViewModel,
assign: "confirmationViewModel",
},
{
method: checkUserCompletedSummary,
assign: "userCompletedSummary",
},
preHandlers.retryPay,
preHandlers.handleUserWithConfirmationViewModel,
preHandlers.checkUserCompletedSummary,
],
handler: async (request: HapiRequest, h: HapiResponseToolkit) => {
const { statusService, cacheService } = request.services([]);

const { params } = request;
const form = server.app.forms[params.id];

Expand Down Expand Up @@ -97,6 +107,29 @@ const index = {
return redirectTo(request, h, res._links.next_url.href);
},
});

server.route({
method: "get",
path: "/{id}/status/payment-skip-warning",
options: {
pre: [preHandlers.checkUserCompletedSummary],
handler: paymentSkippedWarning,
},
});

server.route({
method: "post",
path: "/{id}/status/payment-skip-warning",
options: {
handler: continueToPayAfterPaymentSkippedWarning,
validate: {
payload: Joi.object({
action: Joi.string().valid("pay").required(),
crumb: Joi.string(),
}),
},
},
});
},
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HapiRequest, HapiResponseToolkit } from "server/types";
import { FormModel } from "server/plugins/engine/models";

export async function paymentSkippedWarning(
request: HapiRequest,
h: HapiResponseToolkit
) {
const form: FormModel = request.server.app.forms[request.params.id];
const { allowSubmissionWithoutPayment } = form.feeOptions;

if (allowSubmissionWithoutPayment) {
const { customText } = form.specialPages?.paymentSkippedWarningPage ?? {};
return h
.view("payment-skip-warning", {
customText,
backLink: "./../summary",
})
.takeover();
}

return h.redirect(`${request.params.id}/status`);
}

export async function continueToPayAfterPaymentSkippedWarning(
request: HapiRequest,
h: HapiResponseToolkit
) {
const { cacheService } = request.services([]);
const state = await cacheService.getState(request);

const payState = state.pay;
payState.meta++;
await cacheService.mergeState(request, payState);

const payRedirectUrl = payState.next_url;
return h.redirect(payRedirectUrl);
}
11 changes: 10 additions & 1 deletion runner/src/server/plugins/engine/models/FormModel.feeOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export const DEFAULT_FEE_OPTIONS = {
import { FormDefinition } from "@xgovformbuilder/model";

export const DEFAULT_FEE_OPTIONS: FormDefinition["feeOptions"] = {
/**
* If a payment is required, but the user fails, allow the user to skip payment
* and submit the form. this is the default behaviour.
Expand All @@ -19,4 +21,11 @@ export const DEFAULT_FEE_OPTIONS = {
* A supplementary error message (`customPayErrorMessage`) may also be configured if allowSubmissionWithoutPayment is false.
*/
// customPayErrorMessage: "Custom error message",

/**
* Shows a link (button) below the "Submit and pay" button on the summary page. Clicking this will take the user to a page
* that provides additional messaging, you can warn the user that this may delay their application for example.
* allowSubmissionWithoutPayment must be true for this to be shown.
*/
showPaymentSkippedWarningPage: false,
};
11 changes: 3 additions & 8 deletions runner/src/server/plugins/engine/models/FormModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,8 @@ export class FormModel {
pages: any;
startPage: any;

feeOptions: {
paymentReferenceFormat?: string;
payReturnUrl?: string;
allowSubmissionWithoutPayment: boolean;
maxAttempts: number;
customPayErrorMessage?: "";
};
feeOptions: FormDefinition["feeOptions"];
specialPages: FormDefinition["specialPages"];

constructor(def, options) {
const result = Schema.validate(def, { abortEarly: false });
Expand Down Expand Up @@ -120,7 +115,7 @@ export class FormModel {
// @ts-ignore
this.pages = def.pages.map((pageDef) => this.makePage(pageDef));
this.startPage = this.pages.find((page) => page.path === def.startPage);

this.specialPages = def.specialPages;
this.feeOptions = { ...DEFAULT_FEE_OPTIONS, ...def.feeOptions };
}

Expand Down
Loading

0 comments on commit 9f0bcaf

Please sign in to comment.