@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.
@@ -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
+ }
@@ -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
+ }