@defra/fcp-sfd-frontend-engine 0.15.0 → 0.16.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,61 @@ 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) => {
357
+ if (postcodeLookup) {
358
+ return "/account-address-change";
359
+ } else {
360
+ return "/account-address-enter";
361
+ }
362
+ };
363
+ var addressBackLink = (postcodeLookup) => {
364
+ if (postcodeLookup) {
365
+ return { href: "/account-address-select" };
366
+ } else {
367
+ return { href: "/account-address-enter" };
368
+ }
369
+ };
370
+ var buildAddressLine = (parts) => {
371
+ return parts.filter(Boolean).join(", ") || null;
372
+ };
373
+ var buildStreetLine = (buildingRange, street) => {
374
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
375
+ };
376
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
377
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
378
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
379
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
380
+ county: lookup.county ?? null,
381
+ city: city ?? null,
382
+ country: country ?? null,
383
+ postcode: postcode ?? null
384
+ });
385
+ var formatManualAddress = (manual, city, country, postcode) => ({
386
+ address1: manual.line1 ?? null,
387
+ address2: manual.line2 ?? null,
388
+ address3: manual.line3 ?? null,
389
+ city: city ?? null,
390
+ county: manual.line4 ?? null,
391
+ country: country ?? null,
392
+ postcode: postcode ?? null
393
+ });
339
394
  var formatDisplayAddress = (address) => {
340
395
  const { lookup, manual, postcode, country, city } = address;
341
396
  let addressLines = [];
@@ -371,30 +426,6 @@ var formatDisplayAddress = (address) => {
371
426
  country
372
427
  ];
373
428
  };
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
429
  var formatOriginalAddress = (originalAddress) => {
399
430
  const { lookup, manual, city, country, postcode } = originalAddress;
400
431
  return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
@@ -441,21 +472,6 @@ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
441
472
  });
442
473
  return displayAddresses;
443
474
  };
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
475
 
460
476
  // src/presenters/business-details-presenter.js
461
477
  var getActionText = (value) => {
@@ -487,7 +503,9 @@ var presenters = {
487
503
  getActionText,
488
504
  formatCph,
489
505
  formatCphText,
490
- formatBusinessAddress
506
+ formatBusinessAddress,
507
+ addressBackLink,
508
+ addressChangeLink
491
509
  };
492
510
 
493
511
  // src/utils/joi.js
package/dist/index.js CHANGED
@@ -296,6 +296,61 @@ 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) => {
317
+ if (postcodeLookup) {
318
+ return "/account-address-change";
319
+ } else {
320
+ return "/account-address-enter";
321
+ }
322
+ };
323
+ var addressBackLink = (postcodeLookup) => {
324
+ if (postcodeLookup) {
325
+ return { href: "/account-address-select" };
326
+ } else {
327
+ return { href: "/account-address-enter" };
328
+ }
329
+ };
330
+ var buildAddressLine = (parts) => {
331
+ return parts.filter(Boolean).join(", ") || null;
332
+ };
333
+ var buildStreetLine = (buildingRange, street) => {
334
+ return [buildingRange, street].filter(Boolean).join(" ") || null;
335
+ };
336
+ var formatLookupAddress = (lookup, city, country, postcode) => ({
337
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
338
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
339
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
340
+ county: lookup.county ?? null,
341
+ city: city ?? null,
342
+ country: country ?? null,
343
+ postcode: postcode ?? null
344
+ });
345
+ var formatManualAddress = (manual, city, country, postcode) => ({
346
+ address1: manual.line1 ?? null,
347
+ address2: manual.line2 ?? null,
348
+ address3: manual.line3 ?? null,
349
+ city: city ?? null,
350
+ county: manual.line4 ?? null,
351
+ country: country ?? null,
352
+ postcode: postcode ?? null
353
+ });
299
354
  var formatDisplayAddress = (address) => {
300
355
  const { lookup, manual, postcode, country, city } = address;
301
356
  let addressLines = [];
@@ -331,30 +386,6 @@ var formatDisplayAddress = (address) => {
331
386
  country
332
387
  ];
333
388
  };
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
389
  var formatOriginalAddress = (originalAddress) => {
359
390
  const { lookup, manual, city, country, postcode } = originalAddress;
360
391
  return lookup.uprn ? formatLookupAddress(lookup, city, country, postcode) : formatManualAddress(manual, city, country, postcode);
@@ -401,21 +432,6 @@ var formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
401
432
  });
402
433
  return displayAddresses;
403
434
  };
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
435
 
420
436
  // src/presenters/business-details-presenter.js
421
437
  var getActionText = (value) => {
@@ -447,7 +463,9 @@ var presenters = {
447
463
  getActionText,
448
464
  formatCph,
449
465
  formatCphText,
450
- formatBusinessAddress
466
+ formatBusinessAddress,
467
+ addressBackLink,
468
+ addressChangeLink
451
469
  };
452
470
 
453
471
  // 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.16.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,254 @@
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
+ * @returns {string} The URL path for the address change page
10
+ */
11
+ export const addressChangeLink = (postcodeLookup) => {
12
+ if (postcodeLookup) {
13
+ return '/account-address-change'
14
+ } else {
15
+ return '/account-address-enter'
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Generates the back link object for address-related pages.
21
+ *
22
+ * If the user used a postcode lookup, the back link returns to the address selection page.
23
+ * If they manually entered the address, the back link returns to the manual entry page.
24
+ *
25
+ * @param {boolean} postcodeLookup - Whether the user used a postcode lookup to find their address
26
+ * @returns {Object} An object with an `href` property containing the back link URL
27
+ */
28
+ export const addressBackLink = (postcodeLookup) => {
29
+ if (postcodeLookup) {
30
+ return { href: '/account-address-select' }
31
+ } else {
32
+ return { href: '/account-address-enter' }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Combines address parts into a single line, joining non-empty values with commas.
38
+ *
39
+ * @private
40
+ */
41
+ const buildAddressLine = (parts) => {
42
+ return parts.filter(Boolean).join(', ') || null
43
+ }
44
+
45
+ /**
46
+ * Combines building range and street into a single line, joining with a space.
47
+ *
48
+ * @private
49
+ */
50
+ const buildStreetLine = (buildingRange, street) => {
51
+ return [buildingRange, street].filter(Boolean).join(' ') || null
52
+ }
53
+
54
+ /**
55
+ * Formats a lookup address (from address API) into a consistent flat structure.
56
+ *
57
+ * @private
58
+ */
59
+ const formatLookupAddress = (lookup, city, country, postcode) => ({
60
+ address1: buildAddressLine([lookup.pafOrganisationName, lookup.flatName, lookup.buildingName]),
61
+ address2: buildStreetLine(lookup.buildingNumberRange, lookup.street),
62
+ address3: buildAddressLine([lookup.doubleDependentLocality, lookup.dependentLocality]),
63
+ county: lookup.county ?? null,
64
+ city: city ?? null,
65
+ country: country ?? null,
66
+ postcode: postcode ?? null
67
+ })
68
+
69
+ /**
70
+ * Formats a manually entered address into a consistent flat structure.
71
+ *
72
+ * @private
73
+ */
74
+ const formatManualAddress = (manual, city, country, postcode) => ({
75
+ address1: manual.line1 ?? null,
76
+ address2: manual.line2 ?? null,
77
+ address3: manual.line3 ?? null,
78
+ city: city ?? null,
79
+ county: manual.line4 ?? null,
80
+ country: country ?? null,
81
+ postcode: postcode ?? null
82
+ })
83
+
84
+ /**
85
+ * Identify the correct address to use from the DAL response.
86
+ *
87
+ * An address lookup is where a user enters a postcode and selects an address
88
+ * returned from an API. Manual input is when a user chooses to type the address in themselves.
89
+ *
90
+ * If the UPRN is populated, the user has selected an address from the lookup.
91
+ * UPRN is only returned by the lookup API; manual addresses will always have `uprn = null`.
92
+ *
93
+ * If both a building number range and a street are present, they are combined into one line so they display together.
94
+ *
95
+ * City, postcode and country are always appended to the final address array.
96
+ *
97
+ * @param {Object} address - The complete address object
98
+ *
99
+ * @returns {string[]} An array of address fields (either from lookup or manual)
100
+ *
101
+ * @private
102
+ */
103
+ export const formatDisplayAddress = (address) => {
104
+ const { lookup, manual, postcode, country, city } = address
105
+
106
+ let addressLines = []
107
+
108
+ if (lookup.uprn) {
109
+ // If the uprn is populated then the user has selected an address from the lookup
110
+ const buildingAndStreet = [
111
+ lookup.buildingNumberRange,
112
+ lookup.street
113
+ ].filter(Boolean).join(' ')
114
+
115
+ addressLines = [
116
+ lookup.pafOrganisationName,
117
+ lookup.flatName,
118
+ lookup.buildingName,
119
+ buildingAndStreet,
120
+ lookup.doubleDependentLocality,
121
+ lookup.dependentLocality,
122
+ city,
123
+ lookup.county
124
+ ]
125
+ } else {
126
+ // Otherwise the user manually entered the address
127
+ addressLines = [
128
+ manual.line1,
129
+ manual.line2,
130
+ manual.line3,
131
+ city,
132
+ manual.line4, // County
133
+ manual.line5
134
+ ]
135
+ }
136
+
137
+ return [
138
+ ...addressLines.filter(Boolean),
139
+ postcode,
140
+ country
141
+ ]
142
+ }
143
+
144
+ /**
145
+ * Formats an original address fetched from the DAL into a flattened structure
146
+ * suitable for display or form population on the `address-enter` pages.
147
+ *
148
+ * If the address contains a UPRN, it is treated as a lookup address;
149
+ * otherwise, it is treated as a manually entered address.
150
+ *
151
+ * @param {Object} originalAddress - The full address object from the DAL
152
+ *
153
+ * @returns {Object} A flattened address object with consistent keys
154
+ */
155
+ export const formatOriginalAddress = (originalAddress) => {
156
+ const { lookup, manual, city, country, postcode } = originalAddress
157
+ return lookup.uprn
158
+ ? formatLookupAddress(lookup, city, country, postcode)
159
+ : formatManualAddress(manual, city, country, postcode)
160
+ }
161
+
162
+ /**
163
+ * Formats a changed address object into a consistent structure for form display.
164
+ *
165
+ * If the address includes a UPRN, it indicates the user selected it from an address lookup.
166
+ * In that case, the lookup fields are combined and mapped into the manual address format used by the form.
167
+ *
168
+ * If the address does not include a UPRN, it is assumed to be manually entered and returned as-is.
169
+ *
170
+ * @param {Object} changeBusinessAddress - The changed address object to format
171
+ *
172
+ * @returns {Object} A formatted address object with fields `address1`, `address2`, `address3`,
173
+ * `city`, `county`, `country`, and `postcode`.
174
+ */
175
+ export const formatChangedAddress = (changeBusinessAddress) => {
176
+ if (!changeBusinessAddress.uprn) {
177
+ // manual address (no lookup used)
178
+ return changeBusinessAddress
179
+ }
180
+
181
+ const {
182
+ pafOrganisationName,
183
+ flatName,
184
+ buildingName,
185
+ buildingNumberRange,
186
+ street,
187
+ doubleDependentLocality,
188
+ dependentLocality,
189
+ city,
190
+ county,
191
+ country,
192
+ postcode
193
+ } = changeBusinessAddress
194
+
195
+ return {
196
+ address1: buildAddressLine([pafOrganisationName, flatName, buildingName]),
197
+ address2: buildStreetLine(buildingNumberRange, street),
198
+ address3: buildAddressLine([doubleDependentLocality, dependentLocality]),
199
+ city: city ?? null,
200
+ county: county ?? null,
201
+ country: country ?? null,
202
+ postcode: postcode ?? null
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Formats a list of address objects for display in a dropdown menu.
208
+ *
209
+ * Each address object is transformed into an option with:
210
+ * - `value`: a concatenation of the address UPRN and displayAddress
211
+ * - `text`: the formatted display address string
212
+ * - `selected`: true only if it matches the previously picked address
213
+ *
214
+ * A summary option is prepended to the start of the list, showing how many
215
+ * addresses were found. This summary option is selected by default unless
216
+ * a previously picked address exists.
217
+ *
218
+ * This function is shared across presenters that display address selection
219
+ * lists (e.g. personal or business address flows).
220
+ *
221
+ * Note: The `value` combines `uprn` and `displayAddress` to ensure uniqueness.
222
+ * Some addresses (for example, postcode LL55 2NF) have been observed to share
223
+ * the same UPRN, which caused incorrect selections when UPRN alone was used.
224
+ * Concatenating both fields guarantees each dropdown option has a unique value.
225
+ *
226
+ * @param {Array<Object>} addresses - List of address objects with `uprn` and `displayAddress` properties
227
+ * @param {Object} [previouslyPickedAddress] - Optional object representing the address previously selected by the user
228
+ *
229
+ * @returns {Array<Object>} Array of formatted address options ready for display
230
+ */
231
+ export const formatDisplayAddresses = (addresses, previouslyPickedAddress) => {
232
+ const displayAddresses = addresses.map(address => ({
233
+ value: `${address.uprn}${address.displayAddress}`,
234
+ text: address.displayAddress,
235
+ selected:
236
+ previouslyPickedAddress?.uprn === address.uprn &&
237
+ previouslyPickedAddress?.displayAddress === address.displayAddress
238
+ }))
239
+
240
+ // Check if any address is already selected
241
+ const hasSelectedAddress = displayAddresses.some(addr => addr.selected)
242
+
243
+ // Add a display summary option to the beginning of the list
244
+ // e.g. "18 addresses found"
245
+ const text = addresses.length === 1 ? '1 address found' : `${addresses.length} addresses found`
246
+
247
+ displayAddresses.unshift({
248
+ value: 'display',
249
+ text,
250
+ selected: !hasSelectedAddress
251
+ })
252
+
253
+ return displayAddresses
254
+ }
@@ -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
  }