@defra/fcp-sfd-frontend-engine 0.18.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/dist/index.cjs +928 -612
- package/dist/index.js +927 -612
- package/package.json +1 -1
- package/src/constants/country-names.js +8 -0
- package/src/index.js +1 -0
- package/src/mappers/address-lookup-mapper.js +79 -0
- package/src/mappers/mappers.js +3 -1
- package/src/mock-data/mock-os-places-addresses.js +146 -0
- package/src/services/os-places/address-lookup-service.js +202 -0
- package/src/services/os-places/os-places-stub.js +37 -0
- package/src/services/services.js +5 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -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
|
+
}
|
package/src/mappers/mappers.js
CHANGED
|
@@ -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
|
+
]
|
|
@@ -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
|
+
}
|