@defra/fcp-sfd-frontend-engine 0.22.0 → 0.23.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 CHANGED
@@ -102,6 +102,52 @@ var BUSINESS_EMAIL_ADDRESS = "You have updated your business email address";
102
102
  var BUSINESS_NAME = "You have updated your business name";
103
103
  var BUSINESS_PHONE_NUMBERS = "You have updated your business phone numbers";
104
104
 
105
+ // src/constants/interrupter-journey.js
106
+ var PERSONAL_SECTION_FIELD_ORDER = {
107
+ name: ["first", "middle", "last"],
108
+ dob: ["day", "month", "year"],
109
+ address: ["address1", "address2", "address3", "city", "county", "postcode", "country"],
110
+ phone: ["personalTelephone", "personalMobile"],
111
+ email: ["personalEmail"]
112
+ };
113
+ var BUSINESS_SECTION_FIELD_ORDER = {
114
+ name: ["businessName"],
115
+ address: ["address1", "address2", "address3", "city", "county", "postcode", "country"],
116
+ phone: ["businessTelephone", "businessMobile"],
117
+ email: ["businessEmail"],
118
+ vat: ["vatNumber"]
119
+ };
120
+ var PERSONAL_SECTION_ORDER = ["name", "dob", "address", "phone", "email"];
121
+ var BUSINESS_SECTION_ORDER = ["name", "address", "phone", "email", "vat"];
122
+ var PERSONAL_SECTION_LABELS = {
123
+ name: "full name",
124
+ dob: "date of birth",
125
+ address: "personal address",
126
+ phone: "personal phone number",
127
+ email: "personal email address"
128
+ };
129
+ var BUSINESS_SECTION_LABELS = {
130
+ name: "business name",
131
+ address: "business address",
132
+ phone: "at least one business phone number",
133
+ email: "business email address",
134
+ vat: "business VAT number"
135
+ };
136
+ var PERSONAL_UPDATE_TEXT_LABELS = {
137
+ name: "your full name",
138
+ dob: "your date of birth",
139
+ address: "your personal address",
140
+ phone: "at least one personal phone number",
141
+ email: "your personal email address"
142
+ };
143
+ var BUSINESS_UPDATE_TEXT_LABELS = {
144
+ name: "your business name",
145
+ address: "your business address",
146
+ phone: "at least one business phone number",
147
+ email: "your business email address",
148
+ vat: "your business VAT number"
149
+ };
150
+
105
151
  // src/constants/index.js
106
152
  var constants = {
107
153
  statusCodes: {
@@ -139,6 +185,16 @@ var constants = {
139
185
  BUSINESS_EMAIL_ADDRESS,
140
186
  BUSINESS_NAME,
141
187
  BUSINESS_PHONE_NUMBERS
188
+ },
189
+ interrupterJourney: {
190
+ PERSONAL_SECTION_FIELD_ORDER,
191
+ BUSINESS_SECTION_FIELD_ORDER,
192
+ PERSONAL_SECTION_ORDER,
193
+ BUSINESS_SECTION_ORDER,
194
+ PERSONAL_SECTION_LABELS,
195
+ BUSINESS_SECTION_LABELS,
196
+ PERSONAL_UPDATE_TEXT_LABELS,
197
+ BUSINESS_UPDATE_TEXT_LABELS
142
198
  }
143
199
  };
144
200
 
@@ -736,7 +792,7 @@ var updateBusinessNameMutation = `
736
792
 
737
793
  // src/mutations/business/update-business-phone-numbers.js
738
794
  var updateBusinessPhoneNumbersMutation = `
739
- mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneNumbersInput!) {
795
+ mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneInput!) {
740
796
  updateBusinessPhone(input: $input) {
741
797
  business {
742
798
  info {
@@ -1481,11 +1537,117 @@ var nullIfUndefined = (value) => {
1481
1537
  return value ?? null;
1482
1538
  };
1483
1539
 
1540
+ // src/services/check-interrupter-journey-session-service.js
1541
+ var checkInterrupterJourneySessionService = (yar, journeyKey) => {
1542
+ const sessionData = yar.get(journeyKey);
1543
+ if (!sessionData) {
1544
+ return false;
1545
+ }
1546
+ if (Array.isArray(sessionData.orderedSectionsToFix)) {
1547
+ return true;
1548
+ }
1549
+ return false;
1550
+ };
1551
+
1552
+ // src/services/initialise-fix-journey-service.js
1553
+ var initialiseFixJourneyService = (yar, source, journeyType) => {
1554
+ let sessionKey;
1555
+ let sectionOrder = [];
1556
+ if (journeyType === "business") {
1557
+ sessionKey = "businessDetailsValidation";
1558
+ sectionOrder = BUSINESS_SECTION_ORDER;
1559
+ }
1560
+ if (journeyType === "personal") {
1561
+ sessionKey = "personalDetailsValidation";
1562
+ sectionOrder = PERSONAL_SECTION_ORDER;
1563
+ }
1564
+ const sessionData = yar.get(sessionKey);
1565
+ if (!(sessionData == null ? void 0 : sessionData.sectionsNeedingUpdate)) {
1566
+ return sessionData;
1567
+ }
1568
+ const orderedSectionsToFix = orderSectionsToFix(
1569
+ sessionData.sectionsNeedingUpdate,
1570
+ source,
1571
+ sectionOrder
1572
+ );
1573
+ updateSessionData(sessionData, source, orderedSectionsToFix);
1574
+ yar.set(sessionKey, sessionData);
1575
+ return sessionData;
1576
+ };
1577
+ var updateSessionData = (sessionData, source, orderedSectionsToFix) => {
1578
+ sessionData.orderedSectionsToFix = orderedSectionsToFix;
1579
+ delete sessionData.sectionsNeedingUpdate;
1580
+ delete sessionData.personalFixUpdates;
1581
+ delete sessionData.businessFixUpdates;
1582
+ if (source) {
1583
+ sessionData.source = source;
1584
+ }
1585
+ };
1586
+ var orderSectionsToFix = (sectionsNeedingUpdate, source, SECTION_ORDER) => {
1587
+ const sections = SECTION_ORDER.filter((section) => {
1588
+ return sectionsNeedingUpdate.includes(section);
1589
+ });
1590
+ if (!source) {
1591
+ return sections;
1592
+ }
1593
+ if (!SECTION_ORDER.includes(source)) {
1594
+ return sections;
1595
+ }
1596
+ return [
1597
+ source,
1598
+ ...sections.filter((section) => section !== source)
1599
+ ];
1600
+ };
1601
+
1602
+ // src/services/validate-fix-details-service.js
1603
+ var validateFixDetailsService = (payload, orderedSectionsToFix, schemas2) => {
1604
+ const errors = [];
1605
+ for (const section of orderedSectionsToFix) {
1606
+ const schema = schemas2[section];
1607
+ const result = schema.validate(payload, {
1608
+ abortEarly: false,
1609
+ allowUnknown: true
1610
+ });
1611
+ if (result.error) {
1612
+ errors.push(...result.error.details);
1613
+ }
1614
+ }
1615
+ if (errors.length > 0) {
1616
+ return {
1617
+ error: {
1618
+ details: errors
1619
+ }
1620
+ };
1621
+ }
1622
+ return { value: payload };
1623
+ };
1624
+
1625
+ // src/services/set-fix-session-data-service.js
1626
+ var setFixSessionDataService = (yar, sessionData, payload, journeyKey, updateKey) => {
1627
+ const orderedSectionsToFix = sessionData.orderedSectionsToFix;
1628
+ const type = journeyKey === "businessDetailsValidation" ? "business" : "personal";
1629
+ const SECTION_FIELD_ORDER = type === "business" ? BUSINESS_SECTION_FIELD_ORDER : PERSONAL_SECTION_FIELD_ORDER;
1630
+ const fixUpdates = {};
1631
+ for (const section of orderedSectionsToFix) {
1632
+ const fields = SECTION_FIELD_ORDER[section];
1633
+ fixUpdates[section] = {};
1634
+ for (const field of fields) {
1635
+ fixUpdates[section][field] = payload[field] ?? "";
1636
+ }
1637
+ }
1638
+ sessionData[updateKey] = fixUpdates;
1639
+ yar.set(journeyKey, sessionData);
1640
+ };
1641
+
1484
1642
  // src/services/services.js
1485
1643
  var services = {
1486
1644
  addressLookup: addressLookupService,
1487
1645
  buildUprnAddress,
1488
- buildManualAddress
1646
+ buildManualAddress,
1647
+ checkInterrupterJourneySession: checkInterrupterJourneySessionService,
1648
+ initialiseFixJourney: initialiseFixJourneyService,
1649
+ validateFixDetails: validateFixDetailsService,
1650
+ setFixSessionData: setFixSessionDataService
1489
1651
  };
1490
1652
  // Annotate the CommonJS export names for ESM import in node:
1491
1653
  0 && (module.exports = {
package/dist/index.js CHANGED
@@ -61,6 +61,52 @@ var BUSINESS_EMAIL_ADDRESS = "You have updated your business email address";
61
61
  var BUSINESS_NAME = "You have updated your business name";
62
62
  var BUSINESS_PHONE_NUMBERS = "You have updated your business phone numbers";
63
63
 
64
+ // src/constants/interrupter-journey.js
65
+ var PERSONAL_SECTION_FIELD_ORDER = {
66
+ name: ["first", "middle", "last"],
67
+ dob: ["day", "month", "year"],
68
+ address: ["address1", "address2", "address3", "city", "county", "postcode", "country"],
69
+ phone: ["personalTelephone", "personalMobile"],
70
+ email: ["personalEmail"]
71
+ };
72
+ var BUSINESS_SECTION_FIELD_ORDER = {
73
+ name: ["businessName"],
74
+ address: ["address1", "address2", "address3", "city", "county", "postcode", "country"],
75
+ phone: ["businessTelephone", "businessMobile"],
76
+ email: ["businessEmail"],
77
+ vat: ["vatNumber"]
78
+ };
79
+ var PERSONAL_SECTION_ORDER = ["name", "dob", "address", "phone", "email"];
80
+ var BUSINESS_SECTION_ORDER = ["name", "address", "phone", "email", "vat"];
81
+ var PERSONAL_SECTION_LABELS = {
82
+ name: "full name",
83
+ dob: "date of birth",
84
+ address: "personal address",
85
+ phone: "personal phone number",
86
+ email: "personal email address"
87
+ };
88
+ var BUSINESS_SECTION_LABELS = {
89
+ name: "business name",
90
+ address: "business address",
91
+ phone: "at least one business phone number",
92
+ email: "business email address",
93
+ vat: "business VAT number"
94
+ };
95
+ var PERSONAL_UPDATE_TEXT_LABELS = {
96
+ name: "your full name",
97
+ dob: "your date of birth",
98
+ address: "your personal address",
99
+ phone: "at least one personal phone number",
100
+ email: "your personal email address"
101
+ };
102
+ var BUSINESS_UPDATE_TEXT_LABELS = {
103
+ name: "your business name",
104
+ address: "your business address",
105
+ phone: "at least one business phone number",
106
+ email: "your business email address",
107
+ vat: "your business VAT number"
108
+ };
109
+
64
110
  // src/constants/index.js
65
111
  var constants = {
66
112
  statusCodes: {
@@ -98,6 +144,16 @@ var constants = {
98
144
  BUSINESS_EMAIL_ADDRESS,
99
145
  BUSINESS_NAME,
100
146
  BUSINESS_PHONE_NUMBERS
147
+ },
148
+ interrupterJourney: {
149
+ PERSONAL_SECTION_FIELD_ORDER,
150
+ BUSINESS_SECTION_FIELD_ORDER,
151
+ PERSONAL_SECTION_ORDER,
152
+ BUSINESS_SECTION_ORDER,
153
+ PERSONAL_SECTION_LABELS,
154
+ BUSINESS_SECTION_LABELS,
155
+ PERSONAL_UPDATE_TEXT_LABELS,
156
+ BUSINESS_UPDATE_TEXT_LABELS
101
157
  }
102
158
  };
103
159
 
@@ -695,7 +751,7 @@ var updateBusinessNameMutation = `
695
751
 
696
752
  // src/mutations/business/update-business-phone-numbers.js
697
753
  var updateBusinessPhoneNumbersMutation = `
698
- mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneNumbersInput!) {
754
+ mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneInput!) {
699
755
  updateBusinessPhone(input: $input) {
700
756
  business {
701
757
  info {
@@ -1440,11 +1496,117 @@ var nullIfUndefined = (value) => {
1440
1496
  return value ?? null;
1441
1497
  };
1442
1498
 
1499
+ // src/services/check-interrupter-journey-session-service.js
1500
+ var checkInterrupterJourneySessionService = (yar, journeyKey) => {
1501
+ const sessionData = yar.get(journeyKey);
1502
+ if (!sessionData) {
1503
+ return false;
1504
+ }
1505
+ if (Array.isArray(sessionData.orderedSectionsToFix)) {
1506
+ return true;
1507
+ }
1508
+ return false;
1509
+ };
1510
+
1511
+ // src/services/initialise-fix-journey-service.js
1512
+ var initialiseFixJourneyService = (yar, source, journeyType) => {
1513
+ let sessionKey;
1514
+ let sectionOrder = [];
1515
+ if (journeyType === "business") {
1516
+ sessionKey = "businessDetailsValidation";
1517
+ sectionOrder = BUSINESS_SECTION_ORDER;
1518
+ }
1519
+ if (journeyType === "personal") {
1520
+ sessionKey = "personalDetailsValidation";
1521
+ sectionOrder = PERSONAL_SECTION_ORDER;
1522
+ }
1523
+ const sessionData = yar.get(sessionKey);
1524
+ if (!(sessionData == null ? void 0 : sessionData.sectionsNeedingUpdate)) {
1525
+ return sessionData;
1526
+ }
1527
+ const orderedSectionsToFix = orderSectionsToFix(
1528
+ sessionData.sectionsNeedingUpdate,
1529
+ source,
1530
+ sectionOrder
1531
+ );
1532
+ updateSessionData(sessionData, source, orderedSectionsToFix);
1533
+ yar.set(sessionKey, sessionData);
1534
+ return sessionData;
1535
+ };
1536
+ var updateSessionData = (sessionData, source, orderedSectionsToFix) => {
1537
+ sessionData.orderedSectionsToFix = orderedSectionsToFix;
1538
+ delete sessionData.sectionsNeedingUpdate;
1539
+ delete sessionData.personalFixUpdates;
1540
+ delete sessionData.businessFixUpdates;
1541
+ if (source) {
1542
+ sessionData.source = source;
1543
+ }
1544
+ };
1545
+ var orderSectionsToFix = (sectionsNeedingUpdate, source, SECTION_ORDER) => {
1546
+ const sections = SECTION_ORDER.filter((section) => {
1547
+ return sectionsNeedingUpdate.includes(section);
1548
+ });
1549
+ if (!source) {
1550
+ return sections;
1551
+ }
1552
+ if (!SECTION_ORDER.includes(source)) {
1553
+ return sections;
1554
+ }
1555
+ return [
1556
+ source,
1557
+ ...sections.filter((section) => section !== source)
1558
+ ];
1559
+ };
1560
+
1561
+ // src/services/validate-fix-details-service.js
1562
+ var validateFixDetailsService = (payload, orderedSectionsToFix, schemas2) => {
1563
+ const errors = [];
1564
+ for (const section of orderedSectionsToFix) {
1565
+ const schema = schemas2[section];
1566
+ const result = schema.validate(payload, {
1567
+ abortEarly: false,
1568
+ allowUnknown: true
1569
+ });
1570
+ if (result.error) {
1571
+ errors.push(...result.error.details);
1572
+ }
1573
+ }
1574
+ if (errors.length > 0) {
1575
+ return {
1576
+ error: {
1577
+ details: errors
1578
+ }
1579
+ };
1580
+ }
1581
+ return { value: payload };
1582
+ };
1583
+
1584
+ // src/services/set-fix-session-data-service.js
1585
+ var setFixSessionDataService = (yar, sessionData, payload, journeyKey, updateKey) => {
1586
+ const orderedSectionsToFix = sessionData.orderedSectionsToFix;
1587
+ const type = journeyKey === "businessDetailsValidation" ? "business" : "personal";
1588
+ const SECTION_FIELD_ORDER = type === "business" ? BUSINESS_SECTION_FIELD_ORDER : PERSONAL_SECTION_FIELD_ORDER;
1589
+ const fixUpdates = {};
1590
+ for (const section of orderedSectionsToFix) {
1591
+ const fields = SECTION_FIELD_ORDER[section];
1592
+ fixUpdates[section] = {};
1593
+ for (const field of fields) {
1594
+ fixUpdates[section][field] = payload[field] ?? "";
1595
+ }
1596
+ }
1597
+ sessionData[updateKey] = fixUpdates;
1598
+ yar.set(journeyKey, sessionData);
1599
+ };
1600
+
1443
1601
  // src/services/services.js
1444
1602
  var services = {
1445
1603
  addressLookup: addressLookupService,
1446
1604
  buildUprnAddress,
1447
- buildManualAddress
1605
+ buildManualAddress,
1606
+ checkInterrupterJourneySession: checkInterrupterJourneySessionService,
1607
+ initialiseFixJourney: initialiseFixJourneyService,
1608
+ validateFixDetails: validateFixDetailsService,
1609
+ setFixSessionData: setFixSessionDataService
1448
1610
  };
1449
1611
  export {
1450
1612
  constants,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Shared frontend engine used by both the internal and external frontend applications.",
5
5
  "main": "./dist/index.cjs",
6
6
  "exports": {
@@ -39,6 +39,18 @@ import {
39
39
  BUSINESS_PHONE_NUMBERS
40
40
  } from './success-messages.js'
41
41
 
42
+ // Interrupter journey constants
43
+ import {
44
+ PERSONAL_SECTION_FIELD_ORDER,
45
+ BUSINESS_SECTION_FIELD_ORDER,
46
+ PERSONAL_SECTION_ORDER,
47
+ BUSINESS_SECTION_ORDER,
48
+ PERSONAL_SECTION_LABELS,
49
+ BUSINESS_SECTION_LABELS,
50
+ PERSONAL_UPDATE_TEXT_LABELS,
51
+ BUSINESS_UPDATE_TEXT_LABELS
52
+ } from './interrupter-journey.js'
53
+
42
54
  export const constants = {
43
55
  statusCodes: {
44
56
  BAD_REQUEST,
@@ -75,5 +87,15 @@ export const constants = {
75
87
  BUSINESS_EMAIL_ADDRESS,
76
88
  BUSINESS_NAME,
77
89
  BUSINESS_PHONE_NUMBERS
90
+ },
91
+ interrupterJourney: {
92
+ PERSONAL_SECTION_FIELD_ORDER,
93
+ BUSINESS_SECTION_FIELD_ORDER,
94
+ PERSONAL_SECTION_ORDER,
95
+ BUSINESS_SECTION_ORDER,
96
+ PERSONAL_SECTION_LABELS,
97
+ BUSINESS_SECTION_LABELS,
98
+ PERSONAL_UPDATE_TEXT_LABELS,
99
+ BUSINESS_UPDATE_TEXT_LABELS
78
100
  }
79
101
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Constants for the interrupter journey.
3
+ *
4
+ * Defines the field order for personal and business detail sections,
5
+ * display labels, and the consistent ordering of sections for the fix journey.
6
+ *
7
+ * The ordering defined here is used throughout the fix journey to ensure
8
+ * consistent presentation of sections and error ordering.
9
+ *
10
+ * @module interrupter-journey-constants
11
+ */
12
+
13
+ export const PERSONAL_SECTION_FIELD_ORDER = {
14
+ name: ['first', 'middle', 'last'],
15
+ dob: ['day', 'month', 'year'],
16
+ address: ['address1', 'address2', 'address3', 'city', 'county', 'postcode', 'country'],
17
+ phone: ['personalTelephone', 'personalMobile'],
18
+ email: ['personalEmail']
19
+ }
20
+
21
+ export const BUSINESS_SECTION_FIELD_ORDER = {
22
+ name: ['businessName'],
23
+ address: ['address1', 'address2', 'address3', 'city', 'county', 'postcode', 'country'],
24
+ phone: ['businessTelephone', 'businessMobile'],
25
+ email: ['businessEmail'],
26
+ vat: ['vatNumber']
27
+ }
28
+
29
+ export const PERSONAL_SECTION_ORDER = ['name', 'dob', 'address', 'phone', 'email']
30
+ export const BUSINESS_SECTION_ORDER = ['name', 'address', 'phone', 'email', 'vat']
31
+
32
+ export const PERSONAL_SECTION_LABELS = {
33
+ name: 'full name',
34
+ dob: 'date of birth',
35
+ address: 'personal address',
36
+ phone: 'personal phone number',
37
+ email: 'personal email address'
38
+ }
39
+
40
+ export const BUSINESS_SECTION_LABELS = {
41
+ name: 'business name',
42
+ address: 'business address',
43
+ phone: 'at least one business phone number',
44
+ email: 'business email address',
45
+ vat: 'business VAT number'
46
+ }
47
+
48
+ export const PERSONAL_UPDATE_TEXT_LABELS = {
49
+ name: 'your full name',
50
+ dob: 'your date of birth',
51
+ address: 'your personal address',
52
+ phone: 'at least one personal phone number',
53
+ email: 'your personal email address'
54
+ }
55
+
56
+ export const BUSINESS_UPDATE_TEXT_LABELS = {
57
+ name: 'your business name',
58
+ address: 'your business address',
59
+ phone: 'at least one business phone number',
60
+ email: 'your business email address',
61
+ vat: 'your business VAT number'
62
+ }
@@ -1,5 +1,5 @@
1
1
  export const updateBusinessPhoneNumbersMutation = `
2
- mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneNumbersInput!) {
2
+ mutation UpdateBusinessPhoneNumbers($input: UpdateBusinessPhoneInput!) {
3
3
  updateBusinessPhone(input: $input) {
4
4
  business {
5
5
  info {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Validates that an interrupter journey session is still usable.
3
+ *
4
+ * Ensures the expected session structure exists before allowing
5
+ * the user to continue a fix journey.
6
+ *
7
+ * If the session has been cleared or is incomplete (for example,
8
+ * due to multi-tab usage), the journey should be restarted.
9
+ *
10
+ * @module checkInterrupterJourneySessionService
11
+ */
12
+
13
+ /**
14
+ * Returns true if the session contains valid data for the given journey.
15
+ * Returns false if the session is missing or incomplete.
16
+ *
17
+ * @param {Object} yar - Hapi session object
18
+ * @param {string} journeyKey - Session key for the journey (e.g., 'personalDetailsValidation')
19
+ * @returns {boolean} True if session has orderedSectionsToFix array, false otherwise
20
+ */
21
+ const checkInterrupterJourneySessionService = (yar, journeyKey) => {
22
+ const sessionData = yar.get(journeyKey)
23
+
24
+ if (!sessionData) {
25
+ return false
26
+ }
27
+
28
+ if (Array.isArray(sessionData.orderedSectionsToFix)) {
29
+ return true
30
+ }
31
+
32
+ return false
33
+ }
34
+
35
+ export {
36
+ checkInterrupterJourneySessionService
37
+ }
@@ -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
+ }
@@ -1,8 +1,16 @@
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'
3
7
 
4
8
  export const services = {
5
9
  addressLookup: addressLookupService,
6
10
  buildUprnAddress,
7
- buildManualAddress
11
+ buildManualAddress,
12
+ checkInterrupterJourneySession: checkInterrupterJourneySessionService,
13
+ initialiseFixJourney: initialiseFixJourneyService,
14
+ validateFixDetails: validateFixDetailsService,
15
+ setFixSessionData: setFixSessionDataService
8
16
  }
@@ -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
+ }