@defra/fcp-sfd-frontend-engine 0.17.0 → 0.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/fcp-sfd-frontend-engine",
3
- "version": "0.17.0",
3
+ "version": "0.19.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": {
@@ -0,0 +1,8 @@
1
+ export const COUNTRY_NAMES = {
2
+ E: 'ENGLAND',
3
+ W: 'WALES',
4
+ S: 'SCOTLAND',
5
+ N: 'NORTHERN IRELAND',
6
+ L: 'CHANNEL ISLANDS',
7
+ M: 'ISLE OF MAN'
8
+ }
package/src/index.js CHANGED
@@ -4,3 +4,4 @@ export { mutations } from './mutations/mutations.js'
4
4
  export { presenters } from './presenters/presenters.js'
5
5
  export { schemas } from './schemas/schemas.js'
6
6
  export { utils } from './utils/utils.js'
7
+ export { services } from './services/services.js'
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Maps addresses returned by the OS Places API into a format suitable
3
+ * for front-end display and DAL updating.
4
+ * https://docs.os.uk/os-apis/accessing-os-apis/os-places-api/datasets as a reference for the datasets
5
+ *
6
+ * @module addressLookupMapper
7
+ */
8
+
9
+ import { schemas } from '../schemas/schemas.js'
10
+ import { COUNTRY_NAMES } from '../constants/country-names.js'
11
+
12
+ const addressLookupMapper = (addresses) => {
13
+ // Guard against undefined or null addresses (e.g. from upstream bugs)
14
+ if (!Array.isArray(addresses)) {
15
+ return []
16
+ }
17
+
18
+ return addresses.map((address) => {
19
+ const { error } = schemas.osPlaces.addressLookup.validate(address)
20
+
21
+ if (error) {
22
+ return null
23
+ }
24
+
25
+ const {
26
+ UPRN,
27
+ ADDRESS,
28
+ PO_BOX_NUMBER,
29
+ ORGANISATION_NAME,
30
+ DEPARTMENT_NAME,
31
+ SUB_BUILDING_NAME,
32
+ BUILDING_NAME,
33
+ BUILDING_NUMBER,
34
+ DEPENDENT_THOROUGHFARE_NAME,
35
+ THOROUGHFARE_NAME,
36
+ DOUBLE_DEPENDENT_LOCALITY,
37
+ DEPENDENT_LOCALITY,
38
+ POST_TOWN,
39
+ POSTCODE,
40
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION,
41
+ COUNTRY_CODE
42
+ } = address.properties
43
+
44
+ const buildingName = PO_BOX_NUMBER ? `PO BOX ${PO_BOX_NUMBER}` : BUILDING_NAME || null
45
+
46
+ return {
47
+ displayAddress: ADDRESS,
48
+ pafOrganisationName: filterAndJoin([ORGANISATION_NAME, DEPARTMENT_NAME]),
49
+ flatName: SUB_BUILDING_NAME ?? null,
50
+ buildingName,
51
+ buildingNumberRange: BUILDING_NUMBER ?? null,
52
+ street: filterAndJoin([DEPENDENT_THOROUGHFARE_NAME, THOROUGHFARE_NAME]),
53
+ dependentLocality: DEPENDENT_LOCALITY ?? null,
54
+ doubleDependentLocality: DOUBLE_DEPENDENT_LOCALITY ?? null,
55
+ city: POST_TOWN,
56
+ county: formatCounty(LOCAL_CUSTODIAN_CODE_DESCRIPTION, POST_TOWN),
57
+ postcode: POSTCODE,
58
+ country: COUNTRY_NAMES[COUNTRY_CODE] ?? null,
59
+ uprn: UPRN
60
+ }
61
+ }).filter(Boolean)
62
+ }
63
+
64
+ // Remove placeholder county values
65
+ const formatCounty = (localCustodianCodeDescription, postTown) => {
66
+ if (localCustodianCodeDescription === 'ORDNANCE SURVEY' || localCustodianCodeDescription === postTown) {
67
+ return null
68
+ }
69
+
70
+ return localCustodianCodeDescription
71
+ }
72
+
73
+ const filterAndJoin = (addressesProperties) => {
74
+ return addressesProperties.filter(Boolean).join(', ') || null
75
+ }
76
+
77
+ export {
78
+ addressLookupMapper
79
+ }
@@ -2,10 +2,12 @@ import { mapPersonalBusinessDetails } from './personal-business-details-mapper.j
2
2
  import { mapAddress } from './address-mapper.js'
3
3
  import { mapCustomerName } from './customer-name-mapper.js'
4
4
  import { mapBusinessDetails } from './business-details-mapper.js'
5
+ import { addressLookupMapper } from './address-lookup-mapper.js'
5
6
 
6
7
  export const mappers = {
7
8
  personalBusinessDetails: mapPersonalBusinessDetails,
8
9
  address: mapAddress,
9
10
  customerName: mapCustomerName,
10
- businessDetails: mapBusinessDetails
11
+ businessDetails: mapBusinessDetails,
12
+ addressLookup: addressLookupMapper
11
13
  }
@@ -0,0 +1,146 @@
1
+ export const mockAddresses = [
2
+ {
3
+ properties: {
4
+ UPRN: '10000001',
5
+ ADDRESS: 'APARTMENT 1, REDBRIDGE HOUSE, 1 ROSE COURT, LONDON, SW1A 1AA',
6
+ SUB_BUILDING_NAME: 'APARTMENT 1',
7
+ BUILDING_NAME: 'REDBRIDGE HOUSE',
8
+ BUILDING_NUMBER: '1',
9
+ THOROUGHFARE_NAME: 'ROSE COURT',
10
+ POST_TOWN: 'LONDON',
11
+ POSTCODE: 'SW1A 1AA',
12
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER LONDON',
13
+ COUNTRY_CODE: 'E'
14
+ }
15
+ },
16
+ {
17
+ properties: {
18
+ UPRN: '10000002',
19
+ ADDRESS: 'FLAT 2B, KINGSLEY APARTMENTS, 12 VICTORIA SQUARE, LONDON, SW1A 1AA',
20
+ SUB_BUILDING_NAME: 'FLAT 2B',
21
+ BUILDING_NAME: 'KINGSLEY APARTMENTS',
22
+ BUILDING_NUMBER: '12',
23
+ THOROUGHFARE_NAME: 'VICTORIA SQUARE',
24
+ DEPENDENT_LOCALITY: '',
25
+ POST_TOWN: 'LONDON',
26
+ POSTCODE: 'SW1A 1AA',
27
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER LONDON',
28
+ COUNTRY_CODE: 'E'
29
+ }
30
+ },
31
+ {
32
+ properties: {
33
+ UPRN: '10000003',
34
+ ADDRESS: 'THE GROUND FLOOR, THE OLD GRANARY, 5 CHURCH LANE, LONDON, SW1A 1AA',
35
+ ORGANISATION_NAME: 'OLD GRANARY STUDIOS',
36
+ SUB_BUILDING_NAME: 'GROUND FLOOR',
37
+ BUILDING_NAME: 'THE OLD GRANARY',
38
+ BUILDING_NUMBER: '5',
39
+ THOROUGHFARE_NAME: 'CHURCH LANE',
40
+ POST_TOWN: 'LONDON',
41
+ POSTCODE: 'SW1A 1AA',
42
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER LONDON',
43
+ COUNTRY_CODE: 'E'
44
+ }
45
+ },
46
+ {
47
+ properties: {
48
+ UPRN: '10000004',
49
+ ADDRESS: 'SUITE 3, WELLINGTON HOUSE, 20 QUEEN STREET, LONDON, SW1A 1AA',
50
+ ORGANISATION_NAME: 'WELLINGTON CONSULTING LTD',
51
+ DEPARTMENT_NAME: 'CLIENT SERVICES',
52
+ SUB_BUILDING_NAME: 'SUITE 3',
53
+ BUILDING_NAME: 'WELLINGTON HOUSE',
54
+ BUILDING_NUMBER: '20',
55
+ THOROUGHFARE_NAME: 'QUEEN STREET',
56
+ POST_TOWN: 'LONDON',
57
+ POSTCODE: 'SW1A 1AA',
58
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER LONDON',
59
+ COUNTRY_CODE: 'E'
60
+ }
61
+ },
62
+ {
63
+ properties: {
64
+ UPRN: '10000005',
65
+ ADDRESS: 'UNIT 6, MARKET ROW, 3 MARKET YARD, LONDON, SW1A 1AA',
66
+ ORGANISATION_NAME: 'MARKET ROW TRADERS',
67
+ SUB_BUILDING_NAME: 'UNIT 6',
68
+ BUILDING_NAME: 'MARKET ROW',
69
+ BUILDING_NUMBER: '3',
70
+ THOROUGHFARE_NAME: 'MARKET YARD',
71
+ POST_TOWN: 'LONDON',
72
+ POSTCODE: 'SW1A 1AA',
73
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER LONDON',
74
+ COUNTRY_CODE: 'E'
75
+ }
76
+ },
77
+ {
78
+ properties: {
79
+ UPRN: '20000001',
80
+ ADDRESS: '1 ORCHARD COTTAGES, WESTFIELD LANE, SHEPTON MALLET, BS14 8XX',
81
+ BUILDING_NAME: 'ORCHARD COTTAGES',
82
+ BUILDING_NUMBER: '1',
83
+ THOROUGHFARE_NAME: 'WESTFIELD LANE',
84
+ POST_TOWN: 'SHEPTON MALLET',
85
+ POSTCODE: 'BS14 8XX',
86
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'SOMERSET',
87
+ COUNTRY_CODE: 'E'
88
+ }
89
+ },
90
+ {
91
+ properties: {
92
+ UPRN: '20000002',
93
+ ADDRESS: 'THE STABLES, MANOR FARM, HOLLOW ROAD, SHEPTON MALLET, BS14 8XX',
94
+ ORGANISATION_NAME: 'MANOR FARM',
95
+ SUB_BUILDING_NAME: 'THE STABLES',
96
+ BUILDING_NAME: 'MANOR FARM',
97
+ THOROUGHFARE_NAME: 'HOLLOW ROAD',
98
+ POST_TOWN: 'SHEPTON MALLET',
99
+ POSTCODE: 'BS14 8XX',
100
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'SOMERSET',
101
+ COUNTRY_CODE: 'E'
102
+ }
103
+ },
104
+ {
105
+ properties: {
106
+ UPRN: '30000001',
107
+ ADDRESS: 'FLAT 4, BISHOP COURT, 9 FLEET STREET, LONDON, EC1A 1BB',
108
+ SUB_BUILDING_NAME: 'FLAT 4',
109
+ BUILDING_NAME: 'BISHOP COURT',
110
+ BUILDING_NUMBER: '9',
111
+ THOROUGHFARE_NAME: 'FLEET STREET',
112
+ POST_TOWN: 'LONDON',
113
+ POSTCODE: 'EC1A 1BB',
114
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'CITY OF LONDON',
115
+ COUNTRY_CODE: 'E'
116
+ }
117
+ },
118
+ {
119
+ properties: {
120
+ UPRN: '40000001',
121
+ ADDRESS: '2A PICCADILLY ARCADE, MANCHESTER, M1 1AE',
122
+ ORGANISATION_NAME: 'PICCADILLY ARCADE LTD',
123
+ SUB_BUILDING_NAME: '2A',
124
+ BUILDING_NAME: 'PICCADILLY ARCADE',
125
+ BUILDING_NUMBER: '2A',
126
+ THOROUGHFARE_NAME: 'PICCADILLY',
127
+ POST_TOWN: 'MANCHESTER',
128
+ POSTCODE: 'M1 1AE',
129
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'GREATER MANCHESTER',
130
+ COUNTRY_CODE: 'E'
131
+ }
132
+ },
133
+ {
134
+ properties: {
135
+ UPRN: '50000001',
136
+ ADDRESS: '3 WESTBOURNE TERRACE, LONDON, W1A 0AX',
137
+ BUILDING_NAME: 'WESTBOURNE TERRACE',
138
+ BUILDING_NUMBER: '3',
139
+ THOROUGHFARE_NAME: 'WESTBOURNE TERRACE',
140
+ POST_TOWN: 'LONDON',
141
+ POSTCODE: 'W1A 0AX',
142
+ LOCAL_CUSTODIAN_CODE_DESCRIPTION: 'WESTMINSTER',
143
+ COUNTRY_CODE: 'E'
144
+ }
145
+ }
146
+ ]
@@ -1,13 +1,15 @@
1
1
  import { updateBusinessEmailMutation } from './business/update-business-email.js'
2
2
  import { updateBusinessNameMutation } from './business/update-business-name.js'
3
3
  import { updateCustomerNameMutation } from './personal/update-customer-name.js'
4
- import { updateCustomerEmailMutation } from './personal/update-customer-email.js'
4
+ import { updateCustomerDobMutation } from './personal/update-customer-dob.js'
5
5
  import { updateCustomerPhoneMutation } from './personal/update-customer-phone.js'
6
+ import { updateCustomerEmailMutation } from './personal/update-customer-email.js'
6
7
 
7
8
  export const mutations = {
8
9
  updateBusinessEmail: updateBusinessEmailMutation,
9
10
  updateBusinessName: updateBusinessNameMutation,
10
11
  updateCustomerName: updateCustomerNameMutation,
11
- updateCustomerEmail: updateCustomerEmailMutation,
12
- updateCustomerPhone: updateCustomerPhoneMutation
12
+ updateCustomerDob: updateCustomerDobMutation,
13
+ updateCustomerPhone: updateCustomerPhoneMutation,
14
+ updateCustomerEmail: updateCustomerEmailMutation
13
15
  }
@@ -0,0 +1,11 @@
1
+ export const updateCustomerDobMutation = `
2
+ mutation UpdateCustomerDateOfBirth($input: UpdateCustomerDateOfBirthInput!) {
3
+ updateCustomerDateOfBirth(input: $input) {
4
+ customer {
5
+ info {
6
+ dateOfBirth
7
+ }
8
+ }
9
+ }
10
+ }
11
+ `
@@ -37,6 +37,38 @@ export const formatNumber = (payloadNumber, changedNumber, originalNumber) => {
37
37
  return originalNumber
38
38
  }
39
39
 
40
+ /**
41
+ * Builds date of birth values for the form inputs.
42
+ *
43
+ * Values coming from `payloadDob` are always strings (they come from the form).
44
+ * `changedDob` is saved payload data, so these values are also strings.
45
+ *
46
+ * The original date of birth value comes from the DAL and isn’t a string.
47
+ * When falling back to those values we explicitly convert them to strings
48
+ * so all sources are normalised and safe to use in inputs.
49
+ *
50
+ * Null values are handled to avoid showing 'null' in the UI.
51
+ */
52
+ const formatDatePart = (changed, original) => {
53
+ return changed ?? original?.toString() ?? ''
54
+ }
55
+
56
+ export const formatDateInputValues = (payloadDob, changedDob, originalDob) => {
57
+ if (payloadDob) {
58
+ return {
59
+ day: payloadDob.day ?? '',
60
+ month: payloadDob.month ?? '',
61
+ year: payloadDob.year ?? ''
62
+ }
63
+ }
64
+
65
+ return {
66
+ day: formatDatePart(changedDob?.day, originalDob?.day),
67
+ month: formatDatePart(changedDob?.month, originalDob?.month),
68
+ year: formatDatePart(changedDob?.year, originalDob?.year)
69
+ }
70
+ }
71
+
40
72
  /**
41
73
  * Shared helper used by base presenters to sort validation errors so they
42
74
  * appear in the same order as the sections and fields shown on a Fix List page.
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  formatBackLink,
3
3
  formatNumber,
4
+ formatDateInputValues,
4
5
  sortErrorsBySectionOrder
5
6
  } from './base-presenter.js'
6
7
 
@@ -23,6 +24,7 @@ import {
23
24
  export const presenters = {
24
25
  formatBackLink,
25
26
  formatNumber,
27
+ formatDateInputValues,
26
28
  formatDisplayAddress,
27
29
  formatOriginalAddress,
28
30
  formatChangedAddress,
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Service to fetch and map addresses from the OS Places API based on a UK postcode.
3
+ *
4
+ * The service:
5
+ * - Calls the OS Places API via the official `osdatahub` package to search for addresses for a given postcode
6
+ * - Maps the returned address properties into a format suitable for front-end display and for updating via the DAL.
7
+ *
8
+ * @module addressLookupService
9
+ */
10
+
11
+ import { placesAPI } from 'osdatahub'
12
+ import { addressLookupMapper } from '../../mappers/address-lookup-mapper.js'
13
+ import { mockPostcode } from './os-places-stub.js'
14
+ import { INTERNAL_SERVER_ERROR } from '../../constants/status-codes.js'
15
+
16
+ /**
17
+ * Fetch and map addresses from OS Places API based on postcode.
18
+ *
19
+ * @param {string} postcode - The UK postcode to search for
20
+ * @param {object} osPlacesConfig - OS Places configuration object
21
+ * @param {string} osPlacesConfig.clientId - OS Places API client ID
22
+ * @param {boolean} osPlacesConfig.osPlacesStub - Whether to use mock data
23
+ * @returns {Promise<Array|object>} Array of addresses or error object
24
+ */
25
+ const addressLookupService = async (postcode, osPlacesConfig) => {
26
+ const addresses = await fetchAddressesFromPostcodeLookup(postcode, osPlacesConfig)
27
+
28
+ // If the API call itself failed, return the error object
29
+ if (addresses.error) {
30
+ return addresses
31
+ }
32
+
33
+ // If the API returned successfully but found no addresses for the postcode
34
+ if (!addresses?.length) {
35
+ // Create a Joi-like error object to indicate that the postcode lookup returned no addresses
36
+ return {
37
+ error: [
38
+ {
39
+ message: 'No addresses found for this postcode',
40
+ path: ['postcode']
41
+ }
42
+ ]
43
+ }
44
+ }
45
+
46
+ // Map the raw API response into a format suitable for the front-end
47
+ const mappedAddresses = addressLookupMapper(addresses)
48
+
49
+ return mappedAddresses
50
+ }
51
+
52
+ /**
53
+ * Fetch addresses from the OS Places API or mock data based on configuration.
54
+ *
55
+ * Implements:
56
+ * - Retry logic (3 attempts) for transient errors (network timeouts, 5xx server errors)
57
+ * - Exponential backoff between retries (100ms, 200ms, 400ms)
58
+ * - Standard error formatting for all failures
59
+ *
60
+ * Note: The osdatahub package does not currently support request timeouts.
61
+ * If API calls hang indefinitely, they will eventually fail after 3 retry attempts.
62
+ *
63
+ * @private
64
+ * @param {string} postcode - The UK postcode to search for
65
+ * @param {object} osPlacesConfig - Configuration object containing clientId and osPlacesStub flag
66
+ * @returns {Promise<Array|object>} Array of address features or error object with error property
67
+ */
68
+ const fetchAddressesFromPostcodeLookup = async (postcode, osPlacesConfig) => {
69
+ const MAX_RETRIES = 3
70
+ const INITIAL_BACKOFF_MS = 100
71
+
72
+ for (let attemptNumber = 1; attemptNumber <= MAX_RETRIES; attemptNumber++) {
73
+ try {
74
+ const { clientId, osPlacesStub } = osPlacesConfig
75
+
76
+ // Use mock data for testing if enabled, otherwise call the real OS Places API
77
+ if (osPlacesStub) {
78
+ const response = mockPostcode(postcode)
79
+
80
+ return response.features ?? []
81
+ }
82
+
83
+ const response = await fetchFromPlacesAPI(clientId, postcode)
84
+ return response.features ?? []
85
+ } catch (error) {
86
+ const shouldRetry = isRetryable(error) && (attemptNumber < MAX_RETRIES)
87
+
88
+ if (!shouldRetry) {
89
+ // Either error is permanent, or this was our last attempt
90
+ return buildErrorResponse(error.message)
91
+ }
92
+
93
+ // Wait before retrying with exponential backoff (gives the API time to recover)
94
+ const backoffMs = calculateExponentialBackoff(attemptNumber, INITIAL_BACKOFF_MS)
95
+ await delayBeforeRetry(backoffMs)
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Calculate exponential backoff duration.
102
+ * Attempts: 1, 2, 3 → Wait: 100ms, 200ms, 400ms
103
+ *
104
+ * Example: attempt 2 with 100ms base
105
+ * - exponent = 2 - 1 = 1
106
+ * - powerOf2 = 2^1 = 2
107
+ * - backoff = 100 * 2 = 200ms
108
+ *
109
+ * @private
110
+ * @param {number} attemptNumber - Current attempt (1-indexed)
111
+ * @param {number} baseMs - Initial backoff in milliseconds
112
+ * @returns {number} Milliseconds to wait before next retry
113
+ */
114
+ const calculateExponentialBackoff = (attemptNumber, baseMs) => {
115
+ const exponent = attemptNumber - 1
116
+ const powerOf2 = Math.pow(2, exponent)
117
+ const backoffDuration = baseMs * powerOf2
118
+ return backoffDuration
119
+ }
120
+
121
+ /**
122
+ * Delay before retrying (allows time for API to recover from transient errors).
123
+ *
124
+ * @private
125
+ * @param {number} ms - Milliseconds to wait
126
+ * @returns {Promise<void>}
127
+ */
128
+ function delayBeforeRetry (ms) {
129
+ const callback = (resolve) => {
130
+ setTimeout(resolve, ms)
131
+ }
132
+
133
+ return new Promise(callback)
134
+ }
135
+
136
+ /**
137
+ * Build a standard error response object.
138
+ *
139
+ * @private
140
+ * @param {string} message - Error message
141
+ * @returns {object} Joi-like error object
142
+ */
143
+ const buildErrorResponse = (message) => {
144
+ return {
145
+ error: [
146
+ {
147
+ message: message || 'Failed to fetch addresses',
148
+ path: ['postcode']
149
+ }
150
+ ]
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Fetch from the OS Places API.
156
+ *
157
+ * @private
158
+ * @param {string} clientId - OS Places API client ID
159
+ * @param {string} postcode - The UK postcode to search for
160
+ * @returns {Promise<object>} API response
161
+ * @throws {Error} If API returns an error
162
+ */
163
+ const fetchFromPlacesAPI = async (clientId, postcode) => {
164
+ const response = await placesAPI.postcode(clientId, postcode, { limit: 150 })
165
+
166
+ return response
167
+ }
168
+
169
+ /**
170
+ * Determine if an error is transient (temporary) and should trigger a retry.
171
+ *
172
+ * Transient errors include:
173
+ * - Network errors: ECONNRESET, ECONNREFUSED, ETIMEDOUT
174
+ * - Server errors: 5xx status codes (API is temporarily down)
175
+ *
176
+ * Permanent errors (should NOT retry):
177
+ * - Client errors: 4xx status codes (bad input, auth failure)
178
+ * - Logic errors (bad API key, invalid postcode format)
179
+ *
180
+ * @private
181
+ * @param {Error} error - The error to check
182
+ * @returns {boolean} True if error is transient and we should retry
183
+ */
184
+ const isRetryable = (error) => {
185
+ // Network-level transient errors (temporary connection problems)
186
+ const networkErrors = ['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT']
187
+ if (networkErrors.includes(error.code)) {
188
+ return true
189
+ }
190
+
191
+ // Server error (5xx status codes mean the API is temporarily unavailable)
192
+ if (error.status && error.status >= INTERNAL_SERVER_ERROR) {
193
+ return true
194
+ }
195
+
196
+ // All other errors are permanent (bad input, auth failure, etc.)
197
+ return false
198
+ }
199
+
200
+ export {
201
+ addressLookupService
202
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Mock address data and stub implementation for OS Places API.
3
+ *
4
+ * This module provides mock address data used during development and testing
5
+ * when OS Places API stubbing is enabled.
6
+ *
7
+ * @module osPlacesStub
8
+ */
9
+
10
+ import { mockAddresses } from '../../mock-data/mock-os-places-addresses.js'
11
+
12
+ /**
13
+ * Retrieves mock address data matching a given postcode.
14
+ *
15
+ * This function is used when OS Places API stubbing is enabled.
16
+ * It takes a postcode, formats it to uppercase, and removes all spaces.
17
+ * It then filters the mock address dataset to find all addresses whose
18
+ * postcodes (also normalized by removing spaces and converting to uppercase)
19
+ * match the given postcode.
20
+ *
21
+ * @param {string} postcode - The postcode to search for.
22
+ * @returns {{ features: Array<Object> }} An object containing a `features` array of matching addresses.
23
+ */
24
+ export const mockPostcode = (postcode) => {
25
+ const formattedPostcode = postcode?.toUpperCase().replaceAll(' ', '')
26
+
27
+ // Find addresses that match the formatted postcode (ignoring spaces)
28
+ const matchingAddresses = mockAddresses.filter(address => {
29
+ const addressPostcode = address.properties.POSTCODE?.toUpperCase().replaceAll(' ', '')
30
+
31
+ return addressPostcode === formattedPostcode
32
+ })
33
+
34
+ return {
35
+ features: matchingAddresses
36
+ }
37
+ }
@@ -0,0 +1,5 @@
1
+ import { addressLookupService } from './os-places/address-lookup-service.js'
2
+
3
+ export const services = {
4
+ addressLookup: addressLookupService
5
+ }