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/mid journey exit #1277

Merged
merged 36 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7dc14d8
feat: add /{id}/exit/email and /{id}/exit/status routes
jenbutongit Jun 25, 2024
bee9c18
add exit options definition
jenbutongit Jun 25, 2024
abf12ab
add exitOptions typings
jenbutongit Jun 25, 2024
37b4151
feat add exitService
jenbutongit Jun 26, 2024
1046e56
feat: add pageExitedOn and cacheService.setExitState
jenbutongit Jul 8, 2024
7fbd1a5
tidying prehandlers
jenbutongit Jul 9, 2024
1fa03a6
tidying
jenbutongit Jul 9, 2024
779a7a2
inline documentation
jenbutongit Jul 9, 2024
233d1e1
add documentation
jenbutongit Jul 9, 2024
0d35653
fix docs
jenbutongit Jul 9, 2024
752427d
parseISO string correctly
jenbutongit Jul 9, 2024
e4d9a42
tidy exit state
jenbutongit Jul 9, 2024
c9327be
remove default format
jenbutongit Jul 9, 2024
515c410
add ExitService tests
jenbutongit Jul 11, 2024
87e1584
tidy exit options
jenbutongit Jul 11, 2024
3c15940
tidy exit options
jenbutongit Jul 11, 2024
9dadd83
update FormModel.exitOptions type
jenbutongit Jul 11, 2024
09a4383
tidying and adding inline docs
jenbutongit Jul 11, 2024
d871e94
add refactor notes
jenbutongit Jul 11, 2024
0171ce1
clear user's state
jenbutongit Jul 11, 2024
10d010c
remove exit confirmation page configurations for now
jenbutongit Jul 11, 2024
ffbadb1
fix: allow safelist to be parsed as JSON array
jenbutongit Jul 11, 2024
a377d81
add form name to context
jenbutongit Jul 11, 2024
85cfdc7
fix: save exit response to user's state.
jenbutongit Jul 11, 2024
45ff056
test: add smoke tests
jenbutongit Jul 12, 2024
e9247c5
use updated state for exit response
jenbutongit Jul 12, 2024
45531d7
change validateEmailPostRequest to validateEmailAndSave
jenbutongit Jul 12, 2024
d2cb9a7
update inline docs
jenbutongit Jul 12, 2024
a2a57a7
test(exit): add test for back link, add test for initialised session
jenbutongit Jul 12, 2024
dd4c993
add additional documentation for exiting an initialised form
jenbutongit Jul 12, 2024
6960fd8
add additional docs
jenbutongit Jul 12, 2024
7450e84
more docs
jenbutongit Jul 12, 2024
42e1a06
add step for exit form
jenbutongit Jul 12, 2024
7707432
add utility property "formPath" to exit request
jenbutongit Jul 15, 2024
d87a1d5
add formPath to exit request
jenbutongit Jul 15, 2024
46b571b
add form name to render context
jenbutongit Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions docs/runner/exit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Exiting

This feature allows the user to exit a form. Their data will be sent to the configured URL. It is not the Runner's
responsibility to persist this data.

You can use this feature alongside [session-initialisation.md](./session-initialisation.md) to allow users to "save and return".

If the `exitOptions` property is present on the form, the form will be considered as "allowing exits". You must remove this
to disable it.

If exits are allowed, on all question pages, a button "save and come back later" will be displayed below the continue button.
When the user selects this

1. They will be taken to a page /{id}/exit/email, where /{id} is the form's ID (i.e. the form's file name), which asks for the user's email address.
2. If the email address is valid, a POST request will be made to the configured URL with the user's data
3. If the POST request was successful (2xx) code, the user will be redirected to a page /{id}/exit/status
- If the persistence API returned with a `redirectUrl` in the response body, the user will be redirected to this URL
- If no redirectUrl was returned, the user will be shown a page detailing the email address it was sent to.
- If the persistence API returned with an `expiry` in the response body, the user will be shown a message detailing how long they have to return
4. The user's cache will then be cleared

## Configuration

The exit feature is configured in the form JSON on the `exitOptions` property.

```json5
{
pages: [],
lists: [],
// ..etc
exitOptions: {
url: "your-persistence-api:3005",
format: "WEBHOOK", // can be "WEBHOOK" or "STATE"
},
}
```

`exitOptions.format` can be `WEBHOOK` or `STATE`. This will control in which format the data is sent exitOptions.url.

## Data sent

Depending on which `exitOptions.format` is configured for the form, the format the users' answers are in will be different.

With both formats, the request body will always include `exitState` and `formPath`, which is the form's id.

```json5
{
exitState: {
exitEmailAddress: "[email protected]", // the email address the user entered on /{id}/exit/email
pageExitedOn: "/form-a/your-address", // the page the user chose to exit on
},
formPath: "/form-a",
//...
}
```

### ExitOptions.format - STATE

This mirrors the data in the user's state for the form that they chose to exit on. For example, if the user is filling out
two forms at once, /form-a and /form-b, if the user chose to exit on /form-a, only data from /form-a will be sent.

```json5
{
exitState: {
exitEmailAddress: "[email protected]",
pageExitedOn: "/test/how-many-people",
},
progress: ["/test/uk-passport", "/test/how-many-people"],
checkBeforeYouStart: { ukPassport: true },
applicantDetails: { numberOfApplicants: "1 or fewer" },
formPath: "/test",
}
```

This may be an easier format for the persistence API to parse, however you must convert this data back into the webhook.

In future, the initialise session and webhook output formats may support this "flatter" format for easier persistence.
Note that this data format does not include information like the page or component titles.

### ExitOptions.format - WEBHOOK

This mirrors the same format that data is sent when configuring a webhook output. You may just choose to store the data
as is, until the user decides to return. This makes it simpler calling [session-initialisation.md](./session-initialisation.md).

```json5
{
name: "Digital Form Builder - Runner test",
metadata: {},
questions: [
{
category: "checkBeforeYouStart",
question: "Do you have a UK passport?",
fields: [
{
key: "ukPassport",
title: "Do you have a UK passport?",
type: "list",
},
],
index: 0,
},
{
category: "applicantDetails",
question: "How many applicants are there?",
fields: [
{
key: "numberOfApplicants",
title: "How many applicants are there?",
type: "list",
answer: "1 or fewer",
},
],
index: 0,
},
],
exitState: {
exitEmailAddress: "[email protected]",
pageExitedOn: "/test/how-many-people",
},
formPath: "/test",
}
```

## Persistence API response

The persistence API (exitOptions.url) should return a 2xx status code if the data was successfully persisted. What
"successfully persisted" means depends on your implementation and requirements. This may mean successfully stored in your database,
or successfully inserted into a queue.

The response body must be JSON, and can some or none include the following properties:

- `redirectUrl` - a URL to redirect the user to after the data has been persisted. This can be used to redirect the user to a
"success" page, or to a "hub" or "account" page.
- The redirectUrl must be on the `safelist` (`SAFELIST` environment variable) to allow redirects to it. This is to protect the user from being redirected to an unknown URL.
if the URL is not on the safelist, the user will be shown the runner's success page.
- `expiry` - The ISO date-time string of when the user's data will be deleted. This will be parsed in the format d MMMM yyyy (e.g. 9 July 2024) and shown to the user.

```json5
{
redirectUrl: "https://your-service.service.gov.uk/success",
expiry: "2024-07-09T00:00:00Z",
}
```

The user will be shown a generic error page if the request failed to send, or the API responded with a non 2xx code.

## Initialising session and exiting

When initialising a session you can configure the `callbackUrl`. This will send data to a different URL from
what is configured in the form JSON's webhook output.

Currently, when exiting an initialised session, the data will be sent to the URL configured in `exitOptions.url`. It will not be sent to the `callbackUrl`.

If you need to initialise a session, allow a user to exit, and still be able to identify the user, you should initialise
the session with `metadata`. The metadata should include an identifier so your API can match and merge their data if required.

When exiting the form, the user's last known page is given to you in the `exitState` object in `pageExitedOn` property,
`pageExitedOn` is given in the format `/{formId}/{pagePath}`, e.g. `/test/uk-passport`. If you want to return the user back
to this page wth initialised sessions, you can use the `redirectPath` option in the session initialisation payload.
Note that in the POST `/session/{formId}` payload, the `redirectPath` is relative to the formId, so you must remove `/{formId}`
from `/test/uk-passport` so the user is redirected to the correct page, e.g. `/uk-passport`.
36 changes: 36 additions & 0 deletions e2e/cypress/e2e/runner/exit.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Feature: Exit
As a user,
I want to be able to exit the service,
so that I can save my progress and return at a later date.

Background:
Given the form "exit-expiry" exists

Scenario: Service can be exited with date displayed
When I navigate to the "exit-expiry" form
Then I see "Save and come back later"
When I choose "lisbon"
And I select the button "Save and come back later"
And I enter "[email protected]" for "Enter your email address"
And I select the button "Save and exit"
Then I see "9 July 2024"
And I see "[email protected]"


Scenario: A user can start exiting, then go back to the form
When I navigate to the "exit-expiry" form
Then I see "Save and come back later"
When I choose "lisbon"
And I select the button "Save and come back later"
And I go back
Then I see "First page"

Scenario: An initialised session can be exited
Given the session is initialised for the exit form
When I go to the initialised session URL
And I select the button "Save and come back later"
And I enter "[email protected]" for "Enter your email address"
And I select the button "Save and exit"
Then I see "Your application to exit test has been saved"
# TODO: Mock the API in the e2e process so we can check for correct data sent.

36 changes: 36 additions & 0 deletions e2e/cypress/e2e/runner/exit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { When } from "@badeball/cypress-cucumber-preprocessor";

When("the session is initialised for the exit form", () => {
const url = `${Cypress.env("RUNNER_URL")}/session/exit-expiry`;
cy.request("POST", url, {
options: {
callbackUrl: "http://localhost",
redirectPath: "/second-page",
},
metadata: {
id: "abcdef",
},
questions: [
{
fields: [
{
key: "whichConsulate",
answer: "lisbon",
},
],
index: 0,
},
{
category: "yourDetails",
fields: [
{
key: "fullName",
answer: "first last",
},
],
},
],
}).then((res) => {
cy.wrap(res.body.token).as("token");
});
});
132 changes: 132 additions & 0 deletions e2e/cypress/fixtures/exit-expiry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{
"metadata": {
"caseType": "generic"
},
"startPage": "/first-page",
"pages": [
{
"title": "First page",
"path": "/first-page",
"components": [
{
"name": "whichConsulate",
"options": {},
"type": "RadiosField",
"title": "which consulate",
"list": "IpQxvK",
"schema": {}
}
],
"next": [
{
"path": "/second-page"
}
]
},
{
"section": "yourDetails",
"title": "Second page",
"path": "/second-page",
"components": [
{
"name": "fullName",
"title": "Your full name",
"options": {},
"type": "TextField",
"schema": {}
}
],
"next": [
{
"path": "/summary"
}
]
},
{
"title": "Summary",
"path": "/summary",
"controller": "./pages/summary.js",
"components": []
}
],
"specialPages": {},
"lists": [
{
"title": "which consulate",
"name": "IpQxvK",
"type": "string",
"items": [
{
"text": "lisbon",
"value": "lisbon"
},
{
"text": "portimao",
"value": "portimao"
}
]
}
],
"sections": [
{
"name": "yourDetails",
"title": "Your details"
}
],
"conditions": [
{
"displayName": "isLisbon",
"name": "isLisbon",
"value": {
"name": "isLisbon",
"conditions": [
{
"field": {
"name": "whichConsulate",
"type": "RadiosField",
"display": "which consulate"
},
"operator": "is",
"value": {
"type": "Value",
"value": "lisbon",
"display": "lisbon"
}
}
]
}
},
{
"displayName": "isPortimao",
"name": "isPortimao",
"value": {
"name": "isPortimao",
"conditions": [
{
"field": {
"name": "whichConsulate",
"type": "RadiosField",
"display": "which consulate"
},
"operator": "is",
"value": {
"type": "Value",
"value": "portimao",
"display": "portimao"
}
}
]
}
}
],
"fees": [],
"outputs": [],

"version": 2,
"skipSummary": false,
"exitOptions": {
"format": "WEBHOOK",
"url": "https://61bca17e-fe74-40e0-9c15-a901ad120eca.mock.pstmn.io/exit/expiry"
},
"name": "exit test"
}
7 changes: 7 additions & 0 deletions model/src/data-model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ export type FeeOptions = {
payApiKey?: string | MultipleApiKeys | undefined;
};

export type ExitOptions = {
url: string;
redirectUrl?: string;
format?: "STATE" | "WEBHOOK";
};

/**
* `FormDefinition` is a typescript representation of `Schema`
*/
Expand All @@ -187,4 +193,5 @@ export type FormDefinition = {
specialPages?: SpecialPages;
paymentReferenceFormat?: string;
feeOptions: FeeOptions;
exitOptions: ExitOptions;
};
Loading
Loading