@defra/fcp-sfd-frontend-engine 0.15.0 → 0.17.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
@@ -336,6 +336,77 @@ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
336
336
  }
337
337
  return originalNumber;
338
338
  };
339
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
340
+ const sortedErrors = [];
341
+ for (const section of orderedSectionsToFix) {
342
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
343
+ for (const field of fieldsInSection) {
344
+ if (errors[field]) {
345
+ sortedErrors.push({
346
+ field,
347
+ ...errors[field]
348
+ });
349
+ }
350
+ }
351
+ }
352
+ return sortedErrors;
353
+ };
354
+
355
+ // src/presenters/address-presenter.js
356
+ var addressChangeLink = (postcodeLookup, context) => {
357
+ if (context === "business") {
358
+ if (postcodeLookup) {
359
+ return "/business-address-change";
360
+ }
361
+ return "/business-address-enter";
362
+ }
363
+ if (context === "personal") {
364
+ if (postcodeLookup) {
365
+ return "/account-address-change";
366
+ }
367
+ return "/account-address-enter";
368
+ }
369
+ return null;
370
+ };
371
+ var addressBackLink = (postcodeLookup, context) => {
372
+ if (context === "business") {
373
+ if (postcodeLookup) {
374
+ return { href: "/business-address-select" };
375
+ }
376
+ return { href: "/business-address-enter" };
377
+ }
378
+ if (context === "personal") {
379
+ if (postcodeLookup) {
380
+ return { href: "/account-address-select" };
381
+ }
382
+ return { href: "/account-address-enter" };
383
+ }
384
+ return null;
385
+ };
386
+ var buildAddressLine = (parts) => {
387
+ return parts.filter(Boolean).join(", ") || null;
388
+ };
389
+ var buildStreetLine = (buildingRange, street) => {
390
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
391
+ };
392
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
393
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
394
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
395
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
396
+ county: lookup.county ?? null,
397
+ city: city ?? null,
398
+ country: country ?? null,
399
+ postcode: postcode ?? null
400
+ });
401
+ var formatManualAddress = (manual, city, country, postcode) => ({
402
+ address1: manual.line1 ?? null,
403
+ address2: manual.line2 ?? null,
404
+ address3: manual.line3 ?? null,
405
+ city: city ?? null,
406
+ county: manual.line4 ?? null,
407
+ country: country ?? null,
408
+ postcode: postcode ?? null
409
+ });
339
410
  var formatDisplayAddress = (address) => {
340
411
  const { lookup, manual, postcode, country, city } = address;
341
412
  let addressLines = [];
@@ -371,30 +442,6 @@ var formatDisplayAddress = (address) => {
371
442
  country
372
443
  ];
373
444
  };
374
- var buildAddressLine = (parts) => {
375
- return parts.filter(Boolean).join(", ") || null;
376
- };
377
- var buildStreetLine = (buildingRange, street) => {
378
- return [buildingRange, street].filter(Boolean).join(" ") || null;
379
- };
380
- var formatLookupAddress = (lookup, city, country, postcode) => ({
381
- address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
382
- address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
383
- address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
384
- county: lookup.county ?? null,
385
- city: city ?? null,
386
- country: country ?? null,
387
- postcode: postcode ?? null
388
- });
389
- var formatManualAddress = (manual, city, country, postcode) => ({
390
- address1: manual.line1 ?? null,
391
- address2: manual.line2 ?? null,
392
- address3: manual.line3 ?? null,
393
- city: city ?? null,
394
- county: manual.line4 ?? null,
395
- country: country ?? null,
396
- postcode: postcode ?? null
397
- });
398
445
  var formatOriginalAddress = (originalAddress) => {
399
446
  const { lookup, manual, city, country, postcode } = originalAddress;
400
447
  return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
@@ -441,21 +488,6 @@ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
441
488
  });
442
489
  return displayAddresses;
443
490
  };
444
- var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
445
- const sortedErrors = [];
446
- for (const section of orderedSectionsToFix) {
447
- const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
448
- for (const field of fieldsInSection) {
449
- if (errors[field]) {
450
- sortedErrors.push({
451
- field,
452
- ...errors[field]
453
- });
454
- }
455
- }
456
- }
457
- return sortedErrors;
458
- };
459
491
 
460
492
  // src/presenters/business-details-presenter.js
461
493
  var getActionText = (value) => {
@@ -487,7 +519,9 @@ var presenters = {
487
519
  getActionText,
488
520
  formatCph,
489
521
  formatCphText,
490
- formatBusinessAddress
522
+ formatBusinessAddress,
523
+ addressBackLink,
524
+ addressChangeLink
491
525
  };
492
526
 
493
527
  // src/utils/joi.js
package/dist/index.js CHANGED
@@ -296,6 +296,77 @@ var formatNumber = (payloadNumber, changedNumber, originalNumber) => {
296
296
  }
297
297
  return originalNumber;
298
298
  };
299
+ var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
300
+ const sortedErrors = [];
301
+ for (const section of orderedSectionsToFix) {
302
+ const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
303
+ for (const field of fieldsInSection) {
304
+ if (errors[field]) {
305
+ sortedErrors.push({
306
+ field,
307
+ ...errors[field]
308
+ });
309
+ }
310
+ }
311
+ }
312
+ return sortedErrors;
313
+ };
314
+
315
+ // src/presenters/address-presenter.js
316
+ var addressChangeLink = (postcodeLookup, context) => {
317
+ if (context === "business") {
318
+ if (postcodeLookup) {
319
+ return "/business-address-change";
320
+ }
321
+ return "/business-address-enter";
322
+ }
323
+ if (context === "personal") {
324
+ if (postcodeLookup) {
325
+ return "/account-address-change";
326
+ }
327
+ return "/account-address-enter";
328
+ }
329
+ return null;
330
+ };
331
+ var addressBackLink = (postcodeLookup, context) => {
332
+ if (context === "business") {
333
+ if (postcodeLookup) {
334
+ return { href: "/business-address-select" };
335
+ }
336
+ return { href: "/business-address-enter" };
337
+ }
338
+ if (context === "personal") {
339
+ if (postcodeLookup) {
340
+ return { href: "/account-address-select" };
341
+ }
342
+ return { href: "/account-address-enter" };
343
+ }
344
+ return null;
345
+ };
346
+ var buildAddressLine = (parts) => {
347
+ return parts.filter(Boolean).join(", ") || null;
348
+ };
349
+ var buildStreetLine = (buildingRange, street) => {
350
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
351
+ };
352
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
353
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
354
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
355
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
356
+ county: lookup.county ?? null,
357
+ city: city ?? null,
358
+ country: country ?? null,
359
+ postcode: postcode ?? null
360
+ });
361
+ var formatManualAddress = (manual, city, country, postcode) => ({
362
+ address1: manual.line1 ?? null,
363
+ address2: manual.line2 ?? null,
364
+ address3: manual.line3 ?? null,
365
+ city: city ?? null,
366
+ county: manual.line4 ?? null,
367
+ country: country ?? null,
368
+ postcode: postcode ?? null
369
+ });
299
370
  var formatDisplayAddress = (address) => {
300
371
  const { lookup, manual, postcode, country, city } = address;
301
372
  let addressLines = [];
@@ -331,30 +402,6 @@ var formatDisplayAddress = (address) => {
331
402
  country
332
403
  ];
333
404
  };
334
- var buildAddressLine = (parts) => {
335
- return parts.filter(Boolean).join(", ") || null;
336
- };
337
- var buildStreetLine = (buildingRange, street) => {
338
- return [buildingRange, street].filter(Boolean).join(" ") || null;
339
- };
340
- var formatLookupAddress = (lookup, city, country, postcode) => ({
341
- address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
342
- address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
343
- address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
344
- county: lookup.county ?? null,
345
- city: city ?? null,
346
- country: country ?? null,
347
- postcode: postcode ?? null
348
- });
349
- var formatManualAddress = (manual, city, country, postcode) => ({
350
- address1: manual.line1 ?? null,
351
- address2: manual.line2 ?? null,
352
- address3: manual.line3 ?? null,
353
- city: city ?? null,
354
- county: manual.line4 ?? null,
355
- country: country ?? null,
356
- postcode: postcode ?? null
357
- });
358
405
  var formatOriginalAddress = (originalAddress) => {
359
406
  const { lookup, manual, city, country, postcode } = originalAddress;
360
407
  return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
@@ -401,21 +448,6 @@ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
401
448
  });
402
449
  return displayAddresses;
403
450
  };
404
- var sortErrorsBySectionOrder = (errors, orderedSectionsToFix, SECTION_FIELD_ORDER) => {
405
- const sortedErrors = [];
406
- for (const section of orderedSectionsToFix) {
407
- const fieldsInSection = SECTION_FIELD_ORDER[section] || [];
408
- for (const field of fieldsInSection) {
409
- if (errors[field]) {
410
- sortedErrors.push({
411
- field,
412
- ...errors[field]
413
- });
414
- }
415
- }
416
- }
417
- return sortedErrors;
418
- };
419
451
 
420
452
  // src/presenters/business-details-presenter.js
421
453
  var getActionText = (value) => {
@@ -447,7 +479,9 @@ var presenters = {
447
479
  getActionText,
448
480
  formatCph,
449
481
  formatCphText,
450
- formatBusinessAddress
482
+ formatBusinessAddress,
483
+ addressBackLink,
484
+ addressChangeLink
451
485
  };
452
486
 
453
487
  // src/utils/joi.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.15.0",
3
+ "version": "0.17.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": {
@@ -46,6 +46,7 @@
46
46
  "vitest": "4.1.10"
47
47
  },
48
48
  "dependencies": {
49
- "joi": "18.2.3"
49
+ "joi": "18.2.3",
50
+ "osdatahub": "0.2.2"
50
51
  }
51
52
  }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Determines the correct link for changing an address.
3
+ *
4
+ * If the user previously selected an address from a postcode lookup, they will
5
+ * navigate to the address change page to choose a different address.
6
+ * Otherwise, they navigate to the manual address entry page.
7
+ *
8
+ * @param {boolean} postcodeLookup - Whether the user used a postcode lookup to find their address
9
+ * @param {string} context - The context for the address change ('business' or 'personal')
10
+ * @returns {string} The URL path for the address change page
11
+ */
12
+ export const addressChangeLink = (postcodeLookup, context) => {
13
+ if (context === 'business') {
14
+ if (postcodeLookup) {
15
+ return '/business-address-change'
16
+ }
17
+ return '/business-address-enter'
18
+ }
19
+ if (context === 'personal') {
20
+ if (postcodeLookup) {
21
+ return '/account-address-change'
22
+ }
23
+ return '/account-address-enter'
24
+ }
25
+ return null
26
+ }
27
+
28
+ /**
29
+ * Generates the back link object for address-related pages.
30
+ *
31
+ * If the user used a postcode lookup, the back link returns to the address selection page.
32
+ * If they manually entered the address, the back link returns to the manual entry page.
33
+ *
34
+ * @param {boolean} postcodeLookup - Whether the user used a postcode lookup to find their address
35
+ * @param {string} context - The context for the address change ('business' or 'personal')
36
+ * @returns {Object} An object with an `href` property containing the back link URL
37
+ */
38
+ export const addressBackLink = (postcodeLookup, context) => {
39
+ if (context === 'business') {
40
+ if (postcodeLookup) {
41
+ return { href: '/business-address-select' }
42
+ }
43
+ return { href: '/business-address-enter' }
44
+ }
45
+ if (context === 'personal') {
46
+ if (postcodeLookup) {
47
+ return { href: '/account-address-select' }
48
+ }
49
+ return { href: '/account-address-enter' }
50
+ }
51
+ return null
52
+ }
53
+
54
+ /**
55
+ * Combines address parts into a single line, joining non-empty values with commas.
56
+ *
57
+ * @private
58
+ */
59
+ const buildAddressLine = (parts) => {
60
+ return parts.filter(Boolean).join(', ') || null
61
+ }
62
+
63
+ /**
64
+ * Combines building range and street into a single line, joining with a space.
65
+ *
66
+ * @private
67
+ */
68
+ const buildStreetLine = (buildingRange, street) => {
69
+ return [buildingRange, street].filter(Boolean).join(' ') || null
70
+ }
71
+
72
+ /**
73
+ * Formats a lookup address (from address API) into a consistent flat structure.
74
+ *
75
+ * @private
76
+ */
77
+ const formatLookupAddress = (lookup, city, country, postcode) => ({
78
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
79
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
80
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
81
+ county: lookup.county ?? null,
82
+ city: city ?? null,
83
+ country: country ?? null,
84
+ postcode: postcode ?? null
85
+ })
86
+
87
+ /**
88
+ * Formats a manually entered address into a consistent flat structure.
89
+ *
90
+ * @private
91
+ */
92
+ const formatManualAddress = (manual, city, country, postcode) => ({
93
+ address1: manual.line1 ?? null,
94
+ address2: manual.line2 ?? null,
95
+ address3: manual.line3 ?? null,
96
+ city: city ?? null,
97
+ county: manual.line4 ?? null,
98
+ country: country ?? null,
99
+ postcode: postcode ?? null
100
+ })
101
+
102
+ /**
103
+ * Identify the correct address to use from the DAL response.
104
+ *
105
+ * An address lookup is where a user enters a postcode and selects an address
106
+ * returned from an API. Manual input is when a user chooses to type the address in themselves.
107
+ *
108
+ * If the UPRN is populated, the user has selected an address from the lookup.
109
+ * UPRN is only returned by the lookup API; manual addresses will always have `uprn = null`.
110
+ *
111
+ * If both a building number range and a street are present, they are combined into one line so they display together.
112
+ *
113
+ * City, postcode and country are always appended to the final address array.
114
+ *
115
+ * @param {Object} address - The complete address object
116
+ *
117
+ * @returns {string[]} An array of address fields (either from lookup or manual)
118
+ *
119
+ * @private
120
+ */
121
+ export const formatDisplayAddress = (address) => {
122
+ const { lookup, manual, postcode, country, city } = address
123
+
124
+ let addressLines = []
125
+
126
+ if (lookup.uprn) {
127
+ // If the uprn is populated then the user has selected an address from the lookup
128
+ const buildingAndStreet = [
129
+ lookup.buildingNumberRange,
130
+ lookup.street
131
+ ].filter(Boolean).join(' ')
132
+
133
+ addressLines = [
134
+ lookup.pafOrganisationName,
135
+ lookup.flatName,
136
+ lookup.buildingName,
137
+ buildingAndStreet,
138
+ lookup.doubleDependentLocality,
139
+ lookup.dependentLocality,
140
+ city,
141
+ lookup.county
142
+ ]
143
+ } else {
144
+ // Otherwise the user manually entered the address
145
+ addressLines = [
146
+ manual.line1,
147
+ manual.line2,
148
+ manual.line3,
149
+ city,
150
+ manual.line4, // County
151
+ manual.line5
152
+ ]
153
+ }
154
+
155
+ return [
156
+ ...addressLines.filter(Boolean),
157
+ postcode,
158
+ country
159
+ ]
160
+ }
161
+
162
+ /**
163
+ * Formats an original address fetched from the DAL into a flattened structure
164
+ * suitable for display or form population on the `address-enter` pages.
165
+ *
166
+ * If the address contains a UPRN, it is treated as a lookup address;
167
+ * otherwise, it is treated as a manually entered address.
168
+ *
169
+ * @param {Object} originalAddress - The full address object from the DAL
170
+ *
171
+ * @returns {Object} A flattened address object with consistent keys
172
+ */
173
+ export const formatOriginalAddress = (originalAddress) => {
174
+ const { lookup, manual, city, country, postcode } = originalAddress
175
+ return lookup.uprn
176
+ ? formatLookupAddress(lookup, city, country, postcode)
177
+ : formatManualAddress(manual, city, country, postcode)
178
+ }
179
+
180
+ /**
181
+ * Formats a changed address object into a consistent structure for form display.
182
+ *
183
+ * If the address includes a UPRN, it indicates the user selected it from an address lookup.
184
+ * In that case, the lookup fields are combined and mapped into the manual address format used by the form.
185
+ *
186
+ * If the address does not include a UPRN, it is assumed to be manually entered and returned as-is.
187
+ *
188
+ * @param {Object} changeBusinessAddress - The changed address object to format
189
+ *
190
+ * @returns {Object} A formatted address object with fields `address1`, `address2`, `address3`,
191
+ * `city`, `county`, `country`, and `postcode`.
192
+ */
193
+ export const formatChangedAddress = (changeBusinessAddress) => {
194
+ if (!changeBusinessAddress.uprn) {
195
+ // manual address (no lookup used)
196
+ return changeBusinessAddress
197
+ }
198
+
199
+ const {
200
+ pafOrganisationName,
201
+ flatName,
202
+ buildingName,
203
+ buildingNumberRange,
204
+ street,
205
+ doubleDependentLocality,
206
+ dependentLocality,
207
+ city,
208
+ county,
209
+ country,
210
+ postcode
211
+ } = changeBusinessAddress
212
+
213
+ return {
214
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
215
+ address2: buildStreetLine(buildingNumberRange, street),
216
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
217
+ city: city ?? null,
218
+ county: county ?? null,
219
+ country: country ?? null,
220
+ postcode: postcode ?? null
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Formats a list of address objects for display in a dropdown menu.
226
+ *
227
+ * Each address object is transformed into an option with:
228
+ * - `value`: a concatenation of the address UPRN and displayAddress
229
+ * - `text`: the formatted display address string
230
+ * - `selected`: true only if it matches the previously picked address
231
+ *
232
+ * A summary option is prepended to the start of the list, showing how many
233
+ * addresses were found. This summary option is selected by default unless
234
+ * a previously picked address exists.
235
+ *
236
+ * This function is shared across presenters that display address selection
237
+ * lists (e.g. personal or business address flows).
238
+ *
239
+ * Note: The `value` combines `uprn` and `displayAddress` to ensure uniqueness.
240
+ * Some addresses (for example, postcode LL55 2NF) have been observed to share
241
+ * the same UPRN, which caused incorrect selections when UPRN alone was used.
242
+ * Concatenating both fields guarantees each dropdown option has a unique value.
243
+ *
244
+ * @param {Array<Object>} addresses - List of address objects with `uprn` and `displayAddress` properties
245
+ * @param {Object} [previouslyPickedAddress] - Optional object representing the address previously selected by the user
246
+ *
247
+ * @returns {Array<Object>} Array of formatted address options ready for display
248
+ */
249
+ export const formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
250
+ const displayAddresses = addresses.map(address => ({
251
+ value: `${address.uprn}${address.displayAddress}`,
252
+ text: address.displayAddress,
253
+ selected:
254
+ previouslyPickedAddress?.uprn === address.uprn &&
255
+ previouslyPickedAddress?.displayAddress === address.displayAddress
256
+ }))
257
+
258
+ // Check if any address is already selected
259
+ const hasSelectedAddress = displayAddresses.some(addr => addr.selected)
260
+
261
+ // Add a display summary option to the beginning of the list
262
+ // e.g. "18 addresses found"
263
+ const text = addresses.length === 1 ? '1 address found' : `${addresses.length} addresses found`
264
+
265
+ displayAddresses.unshift({
266
+ value: 'display',
267
+ text,
268
+ selected: !hasSelectedAddress
269
+ })
270
+
271
+ return displayAddresses
272
+ }
@@ -37,227 +37,6 @@ export const formatNumber = (payloadNumber, changedNumber, originalNumber) => {
37
37
  return originalNumber
38
38
  }
39
39
 
40
- /**
41
- * Identify the correct address to use from the DAL response.
42
- *
43
- * An address lookup is where a user enters a postcode and selects an address
44
- * returned from an API. Manual input is when a user chooses to type the address in themselves.
45
- *
46
- * If the UPRN is populated, the user has selected an address from the lookup.
47
- * UPRN is only returned by the lookup API; manual addresses will always have `uprn = null`.
48
- *
49
- * If both a building number range and a street are present, they are combined into one line so they display together.
50
- *
51
- * City, postcode and country are always appended to the final address array.
52
- *
53
- * @param {Object} address - The complete address object
54
- *
55
- * @returns {string[]} An array of address fields (either from lookup or manual)
56
- *
57
- * @private
58
- */
59
-
60
- export const formatDisplayAddress = (address) => {
61
- const { lookup, manual, postcode, country, city } = address
62
-
63
- let addressLines = []
64
-
65
- if (lookup.uprn) {
66
- // If the uprn is populated then the user has selected an address from the lookup
67
- const buildingAndStreet = [
68
- lookup.buildingNumberRange,
69
- lookup.street
70
- ].filter(Boolean).join(' ')
71
-
72
- addressLines = [
73
- lookup.pafOrganisationName,
74
- lookup.flatName,
75
- lookup.buildingName,
76
- buildingAndStreet,
77
- lookup.doubleDependentLocality,
78
- lookup.dependentLocality,
79
- city,
80
- lookup.county
81
- ]
82
- } else {
83
- // Otherwise the user manually entered the address
84
- addressLines = [
85
- manual.line1,
86
- manual.line2,
87
- manual.line3,
88
- city,
89
- manual.line4, // County
90
- manual.line5
91
- ]
92
- }
93
-
94
- return [
95
- ...addressLines.filter(Boolean),
96
- postcode,
97
- country
98
- ]
99
- }
100
-
101
- /**
102
- * Combines address parts into a single line, joining non-empty values with commas.
103
- *
104
- * @private
105
- */
106
- const buildAddressLine = (parts) => {
107
- return parts.filter(Boolean).join(', ') || null
108
- }
109
-
110
- /**
111
- * Combines building range and street into a single line, joining with a space.
112
- *
113
- * @private
114
- */
115
- const buildStreetLine = (buildingRange, street) => {
116
- return [buildingRange, street].filter(Boolean).join(' ') || null
117
- }
118
-
119
- /**
120
- * Formats a lookup address (from address API) into a consistent flat structure.
121
- *
122
- * @private
123
- */
124
- const formatLookupAddress = (lookup, city, country, postcode) => ({
125
- address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
126
- address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
127
- address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
128
- county: lookup.county ?? null,
129
- city: city ?? null,
130
- country: country ?? null,
131
- postcode: postcode ?? null
132
- })
133
-
134
- /**
135
- * Formats a manually entered address into a consistent flat structure.
136
- *
137
- * @private
138
- */
139
- const formatManualAddress = (manual, city, country, postcode) => ({
140
- address1: manual.line1 ?? null,
141
- address2: manual.line2 ?? null,
142
- address3: manual.line3 ?? null,
143
- city: city ?? null,
144
- county: manual.line4 ?? null,
145
- country: country ?? null,
146
- postcode: postcode ?? null
147
- })
148
-
149
- /**
150
- * Formats an original address fetched from the DAL into a flattened structure
151
- * suitable for display or form population on the `address-enter` pages.
152
- *
153
- * If the address contains a UPRN, it is treated as a lookup address;
154
- * otherwise, it is treated as a manually entered address.
155
- *
156
- * @param {Object} originalAddress - The full address object from the DAL
157
- *
158
- * @returns {Object} A flattened address object with consistent keys
159
- */
160
- export const formatOriginalAddress = (originalAddress) => {
161
- const { lookup, manual, city, country, postcode } = originalAddress
162
- return lookup.uprn
163
- ? formatLookupAddress(lookup, city, country, postcode)
164
- : formatManualAddress(manual, city, country, postcode)
165
- }
166
-
167
- /**
168
- * Formats a changed address object into a consistent structure for form display.
169
- *
170
- * If the address includes a UPRN, it indicates the user selected it from an address lookup.
171
- * In that case, the lookup fields are combined and mapped into the manual address format used by the form.
172
- *
173
- * If the address does not include a UPRN, it is assumed to be manually entered and returned as-is.
174
- *
175
- * @param {Object} changeBusinessAddress - The changed address object to format
176
- *
177
- * @returns {Object} A formatted address object with fields `address1`, `address2`, `address3`,
178
- * `city`, `county`, `country`, and `postcode`.
179
- */
180
- export const formatChangedAddress = (changeBusinessAddress) => {
181
- if (!changeBusinessAddress.uprn) {
182
- // manual address (no lookup used)
183
- return changeBusinessAddress
184
- }
185
-
186
- const {
187
- pafOrganisationName,
188
- flatName,
189
- buildingName,
190
- buildingNumberRange,
191
- street,
192
- doubleDependentLocality,
193
- dependentLocality,
194
- city,
195
- county,
196
- country,
197
- postcode
198
- } = changeBusinessAddress
199
-
200
- return {
201
- address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
202
- address2: buildStreetLine(buildingNumberRange, street),
203
- address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
204
- city: city ?? null,
205
- county: county ?? null,
206
- country: country ?? null,
207
- postcode: postcode ?? null
208
- }
209
- }
210
-
211
- /**
212
- * Formats a list of address objects for display in a dropdown menu.
213
- *
214
- * Each address object is transformed into an option with:
215
- * - `value`: a concatenation of the address UPRN and displayAddress
216
- * - `text`: the formatted display address string
217
- * - `selected`: true only if it matches the previously picked address
218
- *
219
- * A summary option is prepended to the start of the list, showing how many
220
- * addresses were found. This summary option is selected by default unless
221
- * a previously picked address exists.
222
- *
223
- * This function is shared across presenters that display address selection
224
- * lists (e.g. personal or business address flows).
225
- *
226
- * Note: The `value` combines `uprn` and `displayAddress` to ensure uniqueness.
227
- * Some addresses (for example, postcode LL55 2NF) have been observed to share
228
- * the same UPRN, which caused incorrect selections when UPRN alone was used.
229
- * Concatenating both fields guarantees each dropdown option has a unique value.
230
- *
231
- * @param {Array<Object>} addresses - List of address objects with `uprn` and `displayAddress` properties
232
- * @param {Object} [previouslyPickedAddress] - Optional object representing the address previously selected by the user
233
- *
234
- * @returns {Array<Object>} Array of formatted address options ready for display
235
- */
236
- export const formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
237
- const displayAddresses = addresses.map(address => ({
238
- value: `${address.uprn}${address.displayAddress}`,
239
- text: address.displayAddress,
240
- selected:
241
- previouslyPickedAddress?.uprn === address.uprn &&
242
- previouslyPickedAddress?.displayAddress === address.displayAddress
243
- }))
244
-
245
- // Check if any address is already selected
246
- const hasSelectedAddress = displayAddresses.some(addr => addr.selected)
247
-
248
- // Add a display summary option to the beginning of the list
249
- // e.g. "18 addresses found"
250
- const text = addresses.length === 1 ? '1 address found' : `${addresses.length} addresses found`
251
-
252
- displayAddresses.unshift({
253
- value: 'display',
254
- text,
255
- selected: !hasSelectedAddress
256
- })
257
-
258
- return displayAddresses
259
- }
260
-
261
40
  /**
262
41
  * Shared helper used by base presenters to sort validation errors so they
263
42
  * appear in the same order as the sections and fields shown on a Fix List page.
@@ -5,7 +5,7 @@
5
5
  * applications and are provided here to avoid duplication.
6
6
  */
7
7
 
8
- import { formatDisplayAddress } from './base-presenter.js'
8
+ import { formatDisplayAddress } from './address-presenter.js'
9
9
 
10
10
  /**
11
11
  * Returns the action text for a summary-list row.
@@ -1,10 +1,6 @@
1
1
  import {
2
2
  formatBackLink,
3
3
  formatNumber,
4
- formatDisplayAddress,
5
- formatOriginalAddress,
6
- formatChangedAddress,
7
- formatDisplayAddresses,
8
4
  sortErrorsBySectionOrder
9
5
  } from './base-presenter.js'
10
6
 
@@ -15,6 +11,15 @@ import {
15
11
  formatBusinessAddress
16
12
  } from './business-details-presenter.js'
17
13
 
14
+ import {
15
+ addressBackLink,
16
+ addressChangeLink,
17
+ formatDisplayAddress,
18
+ formatOriginalAddress,
19
+ formatChangedAddress,
20
+ formatDisplayAddresses
21
+ } from './address-presenter.js'
22
+
18
23
  export const presenters = {
19
24
  formatBackLink,
20
25
  formatNumber,
@@ -26,5 +31,7 @@ export const presenters = {
26
31
  getActionText,
27
32
  formatCph,
28
33
  formatCphText,
29
- formatBusinessAddress
34
+ formatBusinessAddress,
35
+ addressBackLink,
36
+ addressChangeLink
30
37
  }