@defra/fcp-sfd-frontend-engine 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +329 -2
- package/dist/index.js +329 -2
- package/package.json +1 -1
- package/src/constants/index.js +22 -0
- package/src/constants/interrupter-journey.js +62 -0
- package/src/mutations/mutations.js +3 -1
- package/src/mutations/personal/update-customer-details.js +40 -0
- package/src/services/build-fix-success-message-service.js +95 -0
- package/src/services/check-interrupter-journey-session-service.js +37 -0
- package/src/services/initialise-fix-journey-service.js +125 -0
- package/src/services/personal/build-customer-fix-update-variables-service.js +76 -0
- package/src/services/services.js +13 -1
- package/src/services/set-fix-session-data-service.js +124 -0
- package/src/services/validate-fix-details-service.js +59 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialises the fix journey in the user's session.
|
|
3
|
+
*
|
|
4
|
+
* This service is the single place where the fix journey order is defined.
|
|
5
|
+
* It calculates and stores the ordered list of either personal or business
|
|
6
|
+
* detail sections that the user needs to fix.
|
|
7
|
+
*
|
|
8
|
+
* This sets:
|
|
9
|
+
* - orderedSectionsToFix: the ordered list of sections to fix
|
|
10
|
+
* - source: the section the user selected to start the journey (if provided)
|
|
11
|
+
*
|
|
12
|
+
* If a source is provided, that section is placed first, followed by the
|
|
13
|
+
* remaining sections in the order defined by this service.
|
|
14
|
+
*
|
|
15
|
+
* Downstream routes and presenters read this data from the session and
|
|
16
|
+
* don't reorder it.
|
|
17
|
+
*
|
|
18
|
+
* @module initialiseFixJourneyService
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
PERSONAL_SECTION_ORDER,
|
|
23
|
+
BUSINESS_SECTION_ORDER
|
|
24
|
+
} from '../constants/interrupter-journey.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialises the fix journey session data.
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} yar - Hapi session object
|
|
30
|
+
* @param {string} source - The section user clicked to fix (optional)
|
|
31
|
+
* @param {string} journeyType - Either 'personal' or 'business'
|
|
32
|
+
* @returns {Object|undefined} Updated session data, or undefined if session data does not exist
|
|
33
|
+
*/
|
|
34
|
+
const initialiseFixJourneyService = (yar, source, journeyType) => {
|
|
35
|
+
// Determined by journeyType
|
|
36
|
+
let sessionKey
|
|
37
|
+
let sectionOrder = []
|
|
38
|
+
|
|
39
|
+
if (journeyType === 'business') {
|
|
40
|
+
sessionKey = 'businessDetailsValidation'
|
|
41
|
+
sectionOrder = BUSINESS_SECTION_ORDER
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (journeyType === 'personal') {
|
|
45
|
+
sessionKey = 'personalDetailsValidation'
|
|
46
|
+
sectionOrder = PERSONAL_SECTION_ORDER
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sessionData = yar.get(sessionKey)
|
|
50
|
+
|
|
51
|
+
if (!sessionData?.sectionsNeedingUpdate) {
|
|
52
|
+
return sessionData
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const orderedSectionsToFix = orderSectionsToFix(
|
|
56
|
+
sessionData.sectionsNeedingUpdate,
|
|
57
|
+
source,
|
|
58
|
+
sectionOrder
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
updateSessionData(sessionData, source, orderedSectionsToFix)
|
|
62
|
+
|
|
63
|
+
yar.set(sessionKey, sessionData)
|
|
64
|
+
|
|
65
|
+
return sessionData
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Updates session data with the ordered sections and clears temporary fields.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} sessionData - The session data to update
|
|
72
|
+
* @param {string} source - The selected section (optional)
|
|
73
|
+
* @param {Array<string>} orderedSectionsToFix - Ordered list of sections to fix
|
|
74
|
+
*/
|
|
75
|
+
const updateSessionData = (sessionData, source, orderedSectionsToFix) => {
|
|
76
|
+
sessionData.orderedSectionsToFix = orderedSectionsToFix
|
|
77
|
+
|
|
78
|
+
// Clean up temporary fields used only during validation
|
|
79
|
+
delete sessionData.sectionsNeedingUpdate
|
|
80
|
+
delete sessionData.personalFixUpdates
|
|
81
|
+
delete sessionData.businessFixUpdates
|
|
82
|
+
|
|
83
|
+
if (source) {
|
|
84
|
+
sessionData.source = source
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns an ordered list of sections that the user needs to fix.
|
|
90
|
+
*
|
|
91
|
+
* Sections are ordered based on how the personal/business details data
|
|
92
|
+
* is presented on the main page.
|
|
93
|
+
*
|
|
94
|
+
* If a source is provided, that section is moved to the top of the list.
|
|
95
|
+
* Source indicates which link the user clicked to get to the fix list page.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array<string>} sectionsNeedingUpdate - Sections identified as needing fixes
|
|
98
|
+
* @param {string} source - The section user clicked on (optional)
|
|
99
|
+
* @param {Array<string>} SECTION_ORDER - The defined order for sections
|
|
100
|
+
* @returns {Array<string>} Ordered list of sections to fix
|
|
101
|
+
*/
|
|
102
|
+
const orderSectionsToFix = (sectionsNeedingUpdate, source, SECTION_ORDER) => {
|
|
103
|
+
const sections = SECTION_ORDER.filter((section) => {
|
|
104
|
+
return sectionsNeedingUpdate.includes(section)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
if (!source) {
|
|
108
|
+
return sections
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Source comes from a URL query param, so only allow known section keys
|
|
112
|
+
// for this journey type to avoid injecting unexpected values into session state.
|
|
113
|
+
if (!SECTION_ORDER.includes(source)) {
|
|
114
|
+
return sections
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
source,
|
|
119
|
+
...sections.filter(section => section !== source)
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
initialiseFixJourneyService
|
|
125
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds mutation variables for updating a user's personal details via the fix journey.
|
|
3
|
+
* Only includes the sections that actually need updating, using the
|
|
4
|
+
* unified `input` format for the GraphQL mutation.
|
|
5
|
+
*
|
|
6
|
+
* @module buildCustomerFixUpdateVariablesService
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { buildManualAddress } from '../build-address-variables-service.js'
|
|
10
|
+
|
|
11
|
+
const buildCustomerFixUpdateVariablesService = (personalDetails) => {
|
|
12
|
+
const { orderedSectionsToFix, crn } = personalDetails
|
|
13
|
+
|
|
14
|
+
const input = { crn }
|
|
15
|
+
|
|
16
|
+
// Conditionally merge each section into input if it's been updated by the user
|
|
17
|
+
if (orderedSectionsToFix.includes('name') && personalDetails.changePersonalName) {
|
|
18
|
+
Object.assign(input, buildNameInput(personalDetails.changePersonalName))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (orderedSectionsToFix.includes('email') && personalDetails.changePersonalEmail) {
|
|
22
|
+
Object.assign(input, buildEmailInput(personalDetails.changePersonalEmail))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (orderedSectionsToFix.includes('phone') && personalDetails.changePersonalPhoneNumbers) {
|
|
26
|
+
Object.assign(input, buildPhoneInput(personalDetails.changePersonalPhoneNumbers))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (orderedSectionsToFix.includes('dob') && personalDetails.changePersonalDob) {
|
|
30
|
+
Object.assign(input, buildDobInput(personalDetails.changePersonalDob))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (orderedSectionsToFix.includes('address') && personalDetails.changePersonalAddress) {
|
|
34
|
+
Object.assign(input, { address: buildManualAddress(personalDetails.changePersonalAddress) })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { input }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const buildPhoneInput = (change) => {
|
|
41
|
+
return {
|
|
42
|
+
phone: {
|
|
43
|
+
landline: change.personalTelephone ?? null,
|
|
44
|
+
mobile: change.personalMobile ?? null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const buildEmailInput = (change) => {
|
|
50
|
+
return {
|
|
51
|
+
email: {
|
|
52
|
+
address: change.personalEmail
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const buildNameInput = (change) => {
|
|
58
|
+
return {
|
|
59
|
+
first: change.first,
|
|
60
|
+
middle: change.middle ?? null,
|
|
61
|
+
last: change.last
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const buildDobInput = (change) => {
|
|
66
|
+
const { day, month, year } = change
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
// DAL expects dateOfBirth as YYYY-MM-DD e.g. '1990-04-05' not '1990-4-5'
|
|
70
|
+
dateOfBirth: `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
buildCustomerFixUpdateVariablesService
|
|
76
|
+
}
|
package/src/services/services.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { addressLookupService } from './os-places/address-lookup-service.js'
|
|
2
2
|
import { buildUprnAddress, buildManualAddress } from './build-address-variables-service.js'
|
|
3
|
+
import { checkInterrupterJourneySessionService } from './check-interrupter-journey-session-service.js'
|
|
4
|
+
import { initialiseFixJourneyService } from './initialise-fix-journey-service.js'
|
|
5
|
+
import { validateFixDetailsService } from './validate-fix-details-service.js'
|
|
6
|
+
import { setFixSessionDataService } from './set-fix-session-data-service.js'
|
|
7
|
+
import { buildFixSuccessMessageService } from './build-fix-success-message-service.js'
|
|
8
|
+
import { buildCustomerFixUpdateVariablesService } from './personal/build-customer-fix-update-variables-service.js'
|
|
3
9
|
|
|
4
10
|
export const services = {
|
|
5
11
|
addressLookup: addressLookupService,
|
|
6
12
|
buildUprnAddress,
|
|
7
|
-
buildManualAddress
|
|
13
|
+
buildManualAddress,
|
|
14
|
+
checkInterrupterJourneySession: checkInterrupterJourneySessionService,
|
|
15
|
+
initialiseFixJourney: initialiseFixJourneyService,
|
|
16
|
+
validateFixDetails: validateFixDetailsService,
|
|
17
|
+
setFixSessionData: setFixSessionDataService,
|
|
18
|
+
buildFixSuccessMessage: buildFixSuccessMessageService,
|
|
19
|
+
buildCustomerFixUpdateVariables: buildCustomerFixUpdateVariablesService
|
|
8
20
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stores fix journey data on the session.
|
|
3
|
+
*
|
|
4
|
+
* This service is used during the interrupter journey to persist form data
|
|
5
|
+
* as users progress through multiple steps. When users provide updates to
|
|
6
|
+
* invalid personal or business details, their input is stored in the session
|
|
7
|
+
* so it can be reviewed and submitted in later steps.
|
|
8
|
+
*
|
|
9
|
+
* The service automatically determines whether this is a personal or business
|
|
10
|
+
* journey by examining the journeyKey parameter, then structures the payload
|
|
11
|
+
* data according to the appropriate section-based schema.
|
|
12
|
+
*
|
|
13
|
+
* Why this matters:
|
|
14
|
+
* - Users may need to fix multiple detail sections (e.g., name + address + email)
|
|
15
|
+
* - Each section has its own set of fields defined in SECTION_FIELD_ORDER
|
|
16
|
+
* - By organizing data by section, the review page can easily iterate and
|
|
17
|
+
* display each section with its corresponding fields
|
|
18
|
+
* - Session storage preserves form state across page navigations
|
|
19
|
+
*
|
|
20
|
+
* Data structure example:
|
|
21
|
+
* For personal details with sections ['name', 'email']:
|
|
22
|
+
* {
|
|
23
|
+
* personalFixUpdates: {
|
|
24
|
+
* name: { first: 'John', middle: '', last: 'Doe' },
|
|
25
|
+
* email: { personalEmail: 'john@example.com' }
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* @module setFixSessionDataService
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
PERSONAL_SECTION_FIELD_ORDER,
|
|
34
|
+
BUSINESS_SECTION_FIELD_ORDER
|
|
35
|
+
} from '../constants/interrupter-journey.js'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps form payload data into session storage, organized by detail sections.
|
|
39
|
+
*
|
|
40
|
+
* This function takes flat form submission data and restructures it into a
|
|
41
|
+
* hierarchical object where each section (e.g., 'name', 'email', 'address')
|
|
42
|
+
* contains its corresponding fields. Any fields missing from the payload
|
|
43
|
+
* are populated with empty strings, ensuring the session always has a
|
|
44
|
+
* complete structure for downstream presenters and services.
|
|
45
|
+
*
|
|
46
|
+
* The section-based structure is essential because:
|
|
47
|
+
* 1. The fix journey is organized by sections that need fixing
|
|
48
|
+
* 2. The review page displays each section separately
|
|
49
|
+
* 3. The mutation needs to know which fields belong to which section
|
|
50
|
+
* 4. Presenters can easily iterate sections and their fields
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} yar - Hapi session object with get/set methods
|
|
53
|
+
* @param {Object} sessionData - Current session data object to be mutated; expects sessionData.orderedSectionsToFix to exist
|
|
54
|
+
* @param {Object} payload - Flat form submission data (e.g., { first: 'John', last: 'Doe', personalEmail: '...' })
|
|
55
|
+
* Example: ['name', 'address', 'email'] - order matches the presentation order
|
|
56
|
+
* @param {string} journeyKey - Session key that identifies the journey type
|
|
57
|
+
* - 'personalDetailsValidation' for personal details
|
|
58
|
+
* - 'businessDetailsValidation' for business details
|
|
59
|
+
* This key is used to auto-detect the journey type
|
|
60
|
+
* @param {string} updateKey - Object key where the structured updates are stored
|
|
61
|
+
* - 'personalFixUpdates' for personal details
|
|
62
|
+
* - 'businessFixUpdates' for business details
|
|
63
|
+
*
|
|
64
|
+
* @returns {void} Modifies sessionData in place and persists via yar.set()
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // Store personal detail updates
|
|
68
|
+
* setFixSessionDataService(
|
|
69
|
+
* yar,
|
|
70
|
+
* sessionData,
|
|
71
|
+
* { first: 'Jane', last: '', personalEmail: 'jane@example.com' },
|
|
72
|
+
* 'personalDetailsValidation',
|
|
73
|
+
* 'personalFixUpdates'
|
|
74
|
+
* )
|
|
75
|
+
* // Result: sessionData.personalFixUpdates = {
|
|
76
|
+
* // name: { first: 'Jane', middle: '', last: '' },
|
|
77
|
+
* // email: { personalEmail: 'jane@example.com' }
|
|
78
|
+
* // }
|
|
79
|
+
*/
|
|
80
|
+
const setFixSessionDataService = (
|
|
81
|
+
yar,
|
|
82
|
+
sessionData,
|
|
83
|
+
payload,
|
|
84
|
+
journeyKey,
|
|
85
|
+
updateKey
|
|
86
|
+
) => {
|
|
87
|
+
const orderedSectionsToFix = sessionData.orderedSectionsToFix
|
|
88
|
+
// Determine type from journey key
|
|
89
|
+
// This allows the service to auto-select the correct field schema
|
|
90
|
+
const type = journeyKey === 'businessDetailsValidation' ? 'business' : 'personal'
|
|
91
|
+
|
|
92
|
+
// Select the appropriate field order based on type
|
|
93
|
+
// Each type (personal/business) has different fields for each section
|
|
94
|
+
const SECTION_FIELD_ORDER = type === 'business' ? BUSINESS_SECTION_FIELD_ORDER : PERSONAL_SECTION_FIELD_ORDER
|
|
95
|
+
|
|
96
|
+
const fixUpdates = {}
|
|
97
|
+
|
|
98
|
+
// Loop through each section that needs fixing (e.g. name, email)
|
|
99
|
+
// The order is preserved from orderedSectionsToFix
|
|
100
|
+
for (const section of orderedSectionsToFix) {
|
|
101
|
+
const fields = SECTION_FIELD_ORDER[section]
|
|
102
|
+
|
|
103
|
+
fixUpdates[section] = {}
|
|
104
|
+
|
|
105
|
+
// Loop through each field in the section (e.g. firstName, lastName)
|
|
106
|
+
// This ensures all expected fields are present in the session,
|
|
107
|
+
// even if they weren't provided in the form submission
|
|
108
|
+
for (const field of fields) {
|
|
109
|
+
// Map the payload value to the session data, defaulting to an empty string if not provided
|
|
110
|
+
// Empty strings are used as defaults to maintain a consistent structure
|
|
111
|
+
// for downstream presenters and mutation builders
|
|
112
|
+
fixUpdates[section][field] = payload[field] ?? ''
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Store the structured updates in the session under the appropriate key
|
|
117
|
+
sessionData[updateKey] = fixUpdates
|
|
118
|
+
// Persist the updated session data
|
|
119
|
+
yar.set(journeyKey, sessionData)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
setFixSessionDataService
|
|
124
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates fix payload data for personal or business details.
|
|
3
|
+
*
|
|
4
|
+
* This service validates the payload for each section the user is fixing
|
|
5
|
+
* (e.g. name, date of birth, email, business details), running each provided
|
|
6
|
+
* Joi schema separately and collecting any validation errors.
|
|
7
|
+
*
|
|
8
|
+
* The schemas are passed into the service and validated individually rather
|
|
9
|
+
* than being combined into a single Joi schema. Combining schemas previously
|
|
10
|
+
* caused issues with custom validation logic. For example, the date of birth
|
|
11
|
+
* schema includes custom validation to check for real and valid dates. When
|
|
12
|
+
* schemas were combined, failures in other schemas could prevent this custom
|
|
13
|
+
* validation from running, meaning some errors were not surfaced.
|
|
14
|
+
*
|
|
15
|
+
* By validating each schema independently (and allowing unknown fields), we
|
|
16
|
+
* ensure that all section-specific validation logic runs correctly and that
|
|
17
|
+
* all relevant errors are returned to the user.
|
|
18
|
+
*
|
|
19
|
+
* @module validateFixDetailsService
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validates payload against multiple schemas for different sections.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} payload - The form data to validate
|
|
26
|
+
* @param {Array<string>} orderedSectionsToFix - List of sections being fixed
|
|
27
|
+
* @param {Object} schemas - Map of section names to Joi schemas
|
|
28
|
+
* @returns {Object} Joi validation result with combined errors
|
|
29
|
+
*/
|
|
30
|
+
const validateFixDetailsService = (payload, orderedSectionsToFix, schemas) => {
|
|
31
|
+
const errors = []
|
|
32
|
+
|
|
33
|
+
for (const section of orderedSectionsToFix) {
|
|
34
|
+
const schema = schemas[section]
|
|
35
|
+
|
|
36
|
+
const result = schema.validate(payload, {
|
|
37
|
+
abortEarly: false,
|
|
38
|
+
allowUnknown: true
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (result.error) {
|
|
42
|
+
errors.push(...result.error.details)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (errors.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
error: {
|
|
49
|
+
details: errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { value: payload }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
validateFixDetailsService
|
|
59
|
+
}
|