@defra/forms-engine-plugin 4.0.0 → 4.0.1
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/.public/stylesheets/application.min.css +3 -3
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/stylesheets/application.scss +14 -0
- package/.server/config/index.d.ts +1 -0
- package/.server/config/index.js +7 -0
- package/.server/config/index.js.map +1 -1
- package/.server/index.js +6 -2
- package/.server/index.js.map +1 -1
- package/.server/server/constants.d.ts +2 -0
- package/.server/server/constants.js +2 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/forms/components.json +7 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
- package/.server/server/plugins/engine/components/UkAddressField.d.ts +15 -9
- package/.server/server/plugins/engine/components/UkAddressField.js +67 -6
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +6 -3
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.d.ts +2 -0
- package/.server/server/plugins/engine/models/FormModel.js +3 -1
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +2 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +46 -3
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +13 -1
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.js +41 -3
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +19 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/validationHelpers.d.ts +15 -0
- package/.server/server/plugins/engine/validationHelpers.js +29 -0
- package/.server/server/plugins/engine/validationHelpers.js.map +1 -0
- package/.server/server/plugins/engine/views/components/ukaddressfield.html +50 -6
- package/.server/server/plugins/engine/vision.js +3 -1
- package/.server/server/plugins/engine/vision.js.map +1 -1
- package/.server/server/plugins/postcode-lookup/index.d.ts +8 -0
- package/.server/server/plugins/postcode-lookup/index.js +21 -0
- package/.server/server/plugins/postcode-lookup/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/models/index.d.ts +255 -0
- package/.server/server/plugins/postcode-lookup/models/index.js +517 -0
- package/.server/server/plugins/postcode-lookup/models/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/routes/index.d.ts +19 -0
- package/.server/server/plugins/postcode-lookup/routes/index.js +267 -0
- package/.server/server/plugins/postcode-lookup/routes/index.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/service.d.ts +26 -0
- package/.server/server/plugins/postcode-lookup/service.js +148 -0
- package/.server/server/plugins/postcode-lookup/service.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/service.test.js +144 -0
- package/.server/server/plugins/postcode-lookup/service.test.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.d.ts +282 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js +370 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/postcode.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.d.ts +131 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js +195 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/query.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.d.ts +51 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js +52 -0
- package/.server/server/plugins/postcode-lookup/test/__stubs__/uprn.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/types.d.ts +204 -0
- package/.server/server/plugins/postcode-lookup/types.js +144 -0
- package/.server/server/plugins/postcode-lookup/types.js.map +1 -0
- package/.server/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
- package/.server/server/routes/types.d.ts +6 -1
- package/.server/server/routes/types.js +6 -0
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/schemas/index.js +1 -1
- package/.server/server/schemas/index.js.map +1 -1
- package/.server/server/types.d.ts +1 -0
- package/.server/server/types.js.map +1 -1
- package/package.json +2 -2
- package/src/client/stylesheets/application.scss +14 -0
- package/src/config/index.ts +9 -1
- package/src/index.ts +5 -4
- package/src/server/constants.js +2 -0
- package/src/server/forms/components.json +7 -0
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +18 -2
- package/src/server/plugins/engine/components/UkAddressField.test.ts +50 -27
- package/src/server/plugins/engine/components/UkAddressField.ts +91 -8
- package/src/server/plugins/engine/configureEnginePlugin.ts +5 -3
- package/src/server/plugins/engine/models/FormModel.ts +10 -2
- package/src/server/plugins/engine/options.js +2 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +69 -1
- package/src/server/plugins/engine/plugin.ts +13 -1
- package/src/server/plugins/engine/routes/index.test.ts +1 -0
- package/src/server/plugins/engine/routes/index.ts +71 -3
- package/src/server/plugins/engine/types.ts +21 -1
- package/src/server/plugins/engine/validationHelpers.ts +48 -0
- package/src/server/plugins/engine/views/components/ukaddressfield.html +50 -6
- package/src/server/plugins/engine/vision.ts +6 -0
- package/src/server/plugins/postcode-lookup/index.js +21 -0
- package/src/server/plugins/postcode-lookup/models/index.js +549 -0
- package/src/server/plugins/postcode-lookup/routes/index.js +258 -0
- package/src/server/plugins/postcode-lookup/service.js +188 -0
- package/src/server/plugins/postcode-lookup/service.test.js +177 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js +382 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/query.js +200 -0
- package/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js +53 -0
- package/src/server/plugins/postcode-lookup/types.js +143 -0
- package/src/server/plugins/postcode-lookup/views/postcode-lookup-details.html +83 -0
- package/src/server/postcode-lookup.test.ts +64 -0
- package/src/server/routes/types.ts +7 -1
- package/src/server/schemas/index.ts +5 -7
- package/src/server/types.ts +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import Boom from '@hapi/boom'
|
|
2
|
+
import { StatusCodes } from 'http-status-codes'
|
|
3
|
+
import Joi from 'joi'
|
|
4
|
+
|
|
5
|
+
import { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'
|
|
6
|
+
import {
|
|
7
|
+
JOURNEY_BASE_URL,
|
|
8
|
+
detailsPayloadSchema,
|
|
9
|
+
detailsViewModel,
|
|
10
|
+
manualPayloadSchema,
|
|
11
|
+
manualViewModel,
|
|
12
|
+
selectPayloadSchema,
|
|
13
|
+
selectViewModel,
|
|
14
|
+
stepSchema,
|
|
15
|
+
steps
|
|
16
|
+
} from '~/src/server/plugins/postcode-lookup/models/index.js'
|
|
17
|
+
import * as service from '~/src/server/plugins/postcode-lookup/service.js'
|
|
18
|
+
|
|
19
|
+
const viewName = 'postcode-lookup-details'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the session state associated with this journey
|
|
23
|
+
* @param {PostcodeLookupRequest} request
|
|
24
|
+
*/
|
|
25
|
+
function getSessionState(request) {
|
|
26
|
+
/**
|
|
27
|
+
* @type {PostcodeLookupSessionData | undefined}
|
|
28
|
+
*/
|
|
29
|
+
const state = request.yar.get(JOURNEY_BASE_URL)
|
|
30
|
+
|
|
31
|
+
if (!state) {
|
|
32
|
+
throw Boom.internal(`No postcode lookup data found for ${JOURNEY_BASE_URL}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return state
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Flash form component state
|
|
40
|
+
* @param {PostcodeLookupRequest} request - the request
|
|
41
|
+
* @param {string} componentName - the component name
|
|
42
|
+
* @param {Address | PostcodeLookupManualPayload} address - the address from ordnance survey or manually entered
|
|
43
|
+
*/
|
|
44
|
+
function flashComponentState(request, componentName, address) {
|
|
45
|
+
const addressState = {
|
|
46
|
+
addressLine1: address.addressLine1,
|
|
47
|
+
addressLine2: address.addressLine2,
|
|
48
|
+
town: address.town,
|
|
49
|
+
county: address.county,
|
|
50
|
+
postcode: address.postcode,
|
|
51
|
+
uprn: 'uprn' in address && address.uprn ? address.uprn : undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @type {ExternalStateAppendage}
|
|
56
|
+
*/
|
|
57
|
+
const appendage = {
|
|
58
|
+
component: componentName,
|
|
59
|
+
data: addressState
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Initialises and dispatches the request to the postcode lookup journey
|
|
67
|
+
* @param {FormRequestPayload} request - the source page
|
|
68
|
+
* @param {FormResponseToolkit} h - the source page
|
|
69
|
+
* @param {PostcodeLookupDispatchData} initial - the source data
|
|
70
|
+
*/
|
|
71
|
+
export function dispatch(request, h, initial) {
|
|
72
|
+
/**
|
|
73
|
+
* @type {PostcodeLookupSessionData}
|
|
74
|
+
*/
|
|
75
|
+
const data = {
|
|
76
|
+
initial,
|
|
77
|
+
details: { postcodeQuery: '', buildingNameQuery: '' }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
request.yar.set(JOURNEY_BASE_URL, data)
|
|
81
|
+
|
|
82
|
+
const query = initial.step ? `?step=${initial.step}` : ''
|
|
83
|
+
|
|
84
|
+
return h.redirect(`${JOURNEY_BASE_URL}${query}`).code(StatusCodes.SEE_OTHER)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets the postcode lookup routes
|
|
89
|
+
* @param {PostcodeLookupConfiguration} options - ordnance survey api key
|
|
90
|
+
*/
|
|
91
|
+
export function getRoutes(options) {
|
|
92
|
+
return [getRoute(), postRoute(options)]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns {ServerRoute<PostcodeLookupGetRequestRefs>}
|
|
97
|
+
*/
|
|
98
|
+
function getRoute() {
|
|
99
|
+
return {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
path: JOURNEY_BASE_URL,
|
|
102
|
+
handler(request, h) {
|
|
103
|
+
const { query } = request
|
|
104
|
+
const { step } = query
|
|
105
|
+
const session = getSessionState(request)
|
|
106
|
+
|
|
107
|
+
const model =
|
|
108
|
+
step === steps.manual
|
|
109
|
+
? manualViewModel(session)
|
|
110
|
+
: detailsViewModel(session)
|
|
111
|
+
|
|
112
|
+
return h.view(viewName, model)
|
|
113
|
+
},
|
|
114
|
+
options: {
|
|
115
|
+
validate: {
|
|
116
|
+
query: Joi.object()
|
|
117
|
+
.keys({
|
|
118
|
+
step: Joi.string().allow(steps.details, steps.manual).optional()
|
|
119
|
+
})
|
|
120
|
+
.optional()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {PostcodeLookupConfiguration} options
|
|
128
|
+
* @returns {ServerRoute<PostcodeLookupPostRequestRefs>}
|
|
129
|
+
*/
|
|
130
|
+
function postRoute(options) {
|
|
131
|
+
return {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
path: JOURNEY_BASE_URL,
|
|
134
|
+
async handler(request, h) {
|
|
135
|
+
const { payload } = request
|
|
136
|
+
const { step } = payload
|
|
137
|
+
|
|
138
|
+
switch (step) {
|
|
139
|
+
case steps.details: {
|
|
140
|
+
return detailsPostHandler(request, h, options)
|
|
141
|
+
}
|
|
142
|
+
case steps.select: {
|
|
143
|
+
return selectPostHandler(request, h, options)
|
|
144
|
+
}
|
|
145
|
+
case steps.manual: {
|
|
146
|
+
return manualPostHandler(request, h)
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
throw Boom.badRequest(`Invalid step ${step}`)
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
options: {
|
|
153
|
+
validate: {
|
|
154
|
+
payload: Joi.object()
|
|
155
|
+
.keys({
|
|
156
|
+
step: stepSchema
|
|
157
|
+
})
|
|
158
|
+
.unknown(true)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Post handler for the details step
|
|
166
|
+
* @param {PostcodeLookupPostRequest} request
|
|
167
|
+
* @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
|
|
168
|
+
* @param {PostcodeLookupConfiguration} options
|
|
169
|
+
*/
|
|
170
|
+
async function detailsPostHandler(request, h, options) {
|
|
171
|
+
const { payload } = request
|
|
172
|
+
const session = getSessionState(request)
|
|
173
|
+
const { ordnanceSurveyApiKey: apiKey } = options
|
|
174
|
+
const { value: details, error } = detailsPayloadSchema.validate(payload)
|
|
175
|
+
|
|
176
|
+
let model
|
|
177
|
+
|
|
178
|
+
if (error) {
|
|
179
|
+
model = detailsViewModel(session, details, error)
|
|
180
|
+
|
|
181
|
+
return h.view(viewName, model)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { postcodeQuery, buildingNameQuery } = details
|
|
185
|
+
session.details = { postcodeQuery, buildingNameQuery }
|
|
186
|
+
|
|
187
|
+
// Store the updated session
|
|
188
|
+
request.yar.set(JOURNEY_BASE_URL, session)
|
|
189
|
+
|
|
190
|
+
model = await selectViewModel({ session, apiKey })
|
|
191
|
+
|
|
192
|
+
return h.view(viewName, model)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Post handler for the select step
|
|
197
|
+
* @param {PostcodeLookupPostRequest} request
|
|
198
|
+
* @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
|
|
199
|
+
* @param {PostcodeLookupConfiguration} options
|
|
200
|
+
*/
|
|
201
|
+
async function selectPostHandler(request, h, options) {
|
|
202
|
+
const { payload } = request
|
|
203
|
+
const session = getSessionState(request)
|
|
204
|
+
const { ordnanceSurveyApiKey: apiKey } = options
|
|
205
|
+
const { value: select, error } = selectPayloadSchema.validate(payload)
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
const model = await selectViewModel({ session, apiKey }, select, error)
|
|
209
|
+
|
|
210
|
+
return h.view(viewName, model)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const addresses = await service.searchByUPRN(select.uprn, apiKey)
|
|
214
|
+
const property = addresses.at(0)
|
|
215
|
+
|
|
216
|
+
if (!property) {
|
|
217
|
+
throw Boom.internal(`UPRN ${property} not found`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const { componentName, sourceUrl } = session.initial
|
|
221
|
+
flashComponentState(request, componentName, property)
|
|
222
|
+
|
|
223
|
+
// Redirect back to the source form page
|
|
224
|
+
return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Post handler for the manual step
|
|
229
|
+
* @param {PostcodeLookupPostRequest} request
|
|
230
|
+
* @param {ResponseToolkit<PostcodeLookupPostRequestRefs>} h
|
|
231
|
+
*/
|
|
232
|
+
function manualPostHandler(request, h) {
|
|
233
|
+
const { payload } = request
|
|
234
|
+
const session = getSessionState(request)
|
|
235
|
+
|
|
236
|
+
const { value: manual, error } = manualPayloadSchema.validate(payload, {
|
|
237
|
+
abortEarly: false
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
if (error) {
|
|
241
|
+
const model = manualViewModel(session, manual, error)
|
|
242
|
+
|
|
243
|
+
return h.view(viewName, model)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { componentName, sourceUrl } = session.initial
|
|
247
|
+
flashComponentState(request, componentName, manual)
|
|
248
|
+
|
|
249
|
+
// Redirect back to the source form page
|
|
250
|
+
return h.redirect(sourceUrl).code(StatusCodes.SEE_OTHER)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @import { ResponseToolkit, ServerRoute } from '@hapi/hapi'
|
|
255
|
+
* @import { PostcodeLookupManualPayload, Address, PostcodeLookupGetRequestRefs, PostcodeLookupPostRequestRefs, PostcodeLookupRequest, PostcodeLookupPostRequest, PostcodeLookupConfiguration, PostcodeLookupDispatchData, PostcodeLookupSessionData } from '~/src/server/plugins/postcode-lookup/types.js'
|
|
256
|
+
* @import { FormRequestPayload, FormResponseToolkit } from '~/src/server/routes/types.js'
|
|
257
|
+
* @import { ExternalStateAppendage } from '~/src/server/plugins/engine/types.js'
|
|
258
|
+
*/
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { getErrorMessage } from '@defra/forms-model'
|
|
2
|
+
import Boom from '@hapi/boom'
|
|
3
|
+
|
|
4
|
+
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
5
|
+
import { getJson } from '~/src/server/services/httpService.js'
|
|
6
|
+
|
|
7
|
+
const logger = createLogger()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns an empty result set
|
|
11
|
+
*/
|
|
12
|
+
function empty() {
|
|
13
|
+
return []
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Logs OS places errors
|
|
18
|
+
* @param {unknown} err - the error
|
|
19
|
+
* @param {string} endpoint - the OS api endpoint
|
|
20
|
+
*/
|
|
21
|
+
function logErrorAndReturnEmpty(err, endpoint) {
|
|
22
|
+
const msg = `${getErrorMessage(err)} ${(Boom.isBoom(err) && err.data?.payload?.error?.message) ?? ''}`
|
|
23
|
+
|
|
24
|
+
logger.error(err, `Exception occured calling OS places ${endpoint} - ${msg}}`)
|
|
25
|
+
|
|
26
|
+
return empty()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Fetch data from OS API
|
|
31
|
+
* @param {string} url - the url to get address json data from
|
|
32
|
+
* @param {string} endpoint - the url endpoint description for logging
|
|
33
|
+
*/
|
|
34
|
+
async function getAddressData(url, endpoint) {
|
|
35
|
+
const getJsonByType =
|
|
36
|
+
/** @type {typeof getJson<DeliveryPointAddressResult>} */ (getJson)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response = await getJsonByType(url)
|
|
40
|
+
|
|
41
|
+
if (response.error) {
|
|
42
|
+
return logErrorAndReturnEmpty(response.error, endpoint)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const results = response.payload.results
|
|
46
|
+
|
|
47
|
+
if (!Array.isArray(results)) {
|
|
48
|
+
return empty()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return results.map((result) => formatAddress(result.DPA))
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return logErrorAndReturnEmpty(err, endpoint)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* OS places search
|
|
59
|
+
* @param {string} query - the search term
|
|
60
|
+
* @param {string} apiKey - the OS api key
|
|
61
|
+
*/
|
|
62
|
+
export async function searchByQuery(query, apiKey) {
|
|
63
|
+
const endpoint = 'find'
|
|
64
|
+
const url = `https://api.os.uk/search/places/v1/${endpoint}?query=${encodeURIComponent(query)}&key=${apiKey}`
|
|
65
|
+
|
|
66
|
+
return getAddressData(url, endpoint)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* OS postcode search
|
|
71
|
+
* @param {string} postcode - the postcode
|
|
72
|
+
* @param {string} apiKey - the OS api key
|
|
73
|
+
*/
|
|
74
|
+
export async function searchByPostcode(postcode, apiKey) {
|
|
75
|
+
const endpoint = 'postcode'
|
|
76
|
+
const url = `https://api.os.uk/search/places/v1/${endpoint}?postcode=${encodeURIComponent(postcode.replaceAll(/\s/g, ''))}&key=${apiKey}`
|
|
77
|
+
|
|
78
|
+
return getAddressData(url, endpoint)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* OS UPRN search
|
|
83
|
+
* @param {string} uprn - the unique property reference number
|
|
84
|
+
* @param {string} apiKey - the OS api key
|
|
85
|
+
*/
|
|
86
|
+
export async function searchByUPRN(uprn, apiKey) {
|
|
87
|
+
const endpoint = 'uprn'
|
|
88
|
+
const url = `https://api.os.uk/search/places/v1/${endpoint}?uprn=${uprn}&key=${apiKey}`
|
|
89
|
+
|
|
90
|
+
return getAddressData(url, endpoint)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* OS postcode and building name search
|
|
95
|
+
* @param {string} postcodeQuery - the postcode query
|
|
96
|
+
* @param {string} buildingNameQuery - the building name query
|
|
97
|
+
* @param {string} apiKey - the OS api key
|
|
98
|
+
*/
|
|
99
|
+
export async function search(postcodeQuery, buildingNameQuery, apiKey) {
|
|
100
|
+
let addresses = await searchByPostcode(postcodeQuery, apiKey)
|
|
101
|
+
|
|
102
|
+
if (buildingNameQuery) {
|
|
103
|
+
addresses = addresses.filter((item) =>
|
|
104
|
+
item.address.includes(buildingNameQuery.toUpperCase())
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return addresses
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Converts a delivery point address to an address
|
|
113
|
+
* Taken from http://github.com/dwp/find-an-address-plugin/blob/main/utils/getData.js
|
|
114
|
+
* @param {DeliveryPointAddress} dpa
|
|
115
|
+
*/
|
|
116
|
+
function formatAddress(dpa) {
|
|
117
|
+
const addressLine1 = formatAddressLine1(dpa)
|
|
118
|
+
const addressLine2 = formatAddressLine2(dpa)
|
|
119
|
+
const town = titleCase(dpa.POST_TOWN || '')
|
|
120
|
+
const postcode = dpa.POSTCODE || ''
|
|
121
|
+
const lines = [addressLine1, addressLine2, town]
|
|
122
|
+
const formatted = `${lines.filter((i) => !!i).join(', ')}, ${postcode}`
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @type {Address}
|
|
126
|
+
*/
|
|
127
|
+
const address = {
|
|
128
|
+
uprn: dpa.UPRN,
|
|
129
|
+
address: dpa.ADDRESS,
|
|
130
|
+
addressLine1,
|
|
131
|
+
addressLine2,
|
|
132
|
+
town,
|
|
133
|
+
county: '',
|
|
134
|
+
postcode,
|
|
135
|
+
formatted
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return address
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {DeliveryPointAddress} dpa
|
|
143
|
+
*/
|
|
144
|
+
function formatAddressLine1(dpa) {
|
|
145
|
+
return titleCase(
|
|
146
|
+
dpa.ORGANISATION_NAME ||
|
|
147
|
+
dpa.SUB_BUILDING_NAME ||
|
|
148
|
+
dpa.BUILDING_NAME ||
|
|
149
|
+
dpa.BUILDING_NUMBER
|
|
150
|
+
? [
|
|
151
|
+
dpa.ORGANISATION_NAME || '',
|
|
152
|
+
dpa.SUB_BUILDING_NAME || '',
|
|
153
|
+
dpa.BUILDING_NAME || '',
|
|
154
|
+
dpa.BUILDING_NUMBER || ''
|
|
155
|
+
]
|
|
156
|
+
.filter((item) => !!item)
|
|
157
|
+
.join(' ')
|
|
158
|
+
: ''
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {DeliveryPointAddress} dpa
|
|
164
|
+
*/
|
|
165
|
+
function formatAddressLine2(dpa) {
|
|
166
|
+
return titleCase(
|
|
167
|
+
dpa.THOROUGHFARE_NAME || dpa.DEPENDENT_LOCALITY
|
|
168
|
+
? [dpa.THOROUGHFARE_NAME || '', dpa.DEPENDENT_LOCALITY || '']
|
|
169
|
+
.filter((item) => !!item)
|
|
170
|
+
.join(', ')
|
|
171
|
+
: ''
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Title case address
|
|
177
|
+
* @param {string} address
|
|
178
|
+
*/
|
|
179
|
+
function titleCase(address) {
|
|
180
|
+
return address
|
|
181
|
+
.split(' ')
|
|
182
|
+
.map((item) => item.charAt(0).toUpperCase() + item.slice(1).toLowerCase())
|
|
183
|
+
.join(' ')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @import { Address, DeliveryPointAddress, DeliveryPointAddressResult } from '~/src/server/plugins/postcode-lookup/types.js'
|
|
188
|
+
*/
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import Boom from '@hapi/boom'
|
|
2
|
+
|
|
3
|
+
import * as service from '~/src/server/plugins/postcode-lookup/service.js'
|
|
4
|
+
import { result as postcodeResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/postcode.js'
|
|
5
|
+
import { result as queryResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/query.js'
|
|
6
|
+
import { result as uprnResult } from '~/src/server/plugins/postcode-lookup/test/__stubs__/uprn.js'
|
|
7
|
+
import { getJson } from '~/src/server/services/httpService.js'
|
|
8
|
+
|
|
9
|
+
jest.mock('~/src/server/services/httpService.ts')
|
|
10
|
+
|
|
11
|
+
describe('Postcode lookup service', () => {
|
|
12
|
+
describe('searchByPostcode', () => {
|
|
13
|
+
it('should return formatted addresses', async () => {
|
|
14
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
15
|
+
res: /** @type {IncomingMessage} */ ({
|
|
16
|
+
statusCode: 200,
|
|
17
|
+
headers: {}
|
|
18
|
+
}),
|
|
19
|
+
payload: postcodeResult,
|
|
20
|
+
error: undefined
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const results = await service.searchByPostcode('NW1 6XE', 'apikey')
|
|
24
|
+
|
|
25
|
+
expect(results).toHaveLength(10)
|
|
26
|
+
expect(results.at(0)).toEqual({
|
|
27
|
+
address: "EMILIA'S CRAFTED PASTA, 215, BAKER STREET, LONDON, NW1 6XE",
|
|
28
|
+
addressLine1: "Emilia's Crafted Pasta 215",
|
|
29
|
+
addressLine2: 'Baker Street',
|
|
30
|
+
county: '',
|
|
31
|
+
formatted: "Emilia's Crafted Pasta 215, Baker Street, London, NW1 6XE",
|
|
32
|
+
postcode: 'NW1 6XE',
|
|
33
|
+
town: 'London',
|
|
34
|
+
uprn: '10033619968'
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return an empty response when an error is encountered', async () => {
|
|
39
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
40
|
+
res: /** @type {IncomingMessage} */ ({
|
|
41
|
+
statusCode: 300,
|
|
42
|
+
headers: {}
|
|
43
|
+
}),
|
|
44
|
+
payload: undefined,
|
|
45
|
+
error: new Error('Unknown error')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const results = await service.searchByPostcode('NW1 6XE', 'apikey')
|
|
49
|
+
|
|
50
|
+
expect(results).toHaveLength(0)
|
|
51
|
+
expect(results).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should return an empty response when a non 200 response is encountered', async () => {
|
|
55
|
+
jest
|
|
56
|
+
.mocked(getJson)
|
|
57
|
+
.mockRejectedValueOnce(
|
|
58
|
+
Boom.badRequest(
|
|
59
|
+
'OS API error',
|
|
60
|
+
new Error('Invalid postcode segments')
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const results = await service.searchByPostcode(
|
|
65
|
+
'invalid postcode',
|
|
66
|
+
'apikey'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
expect(results).toHaveLength(0)
|
|
70
|
+
expect(results).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return an empty response when no results are returned', async () => {
|
|
74
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
75
|
+
res: /** @type {IncomingMessage} */ ({
|
|
76
|
+
statusCode: 200,
|
|
77
|
+
headers: {}
|
|
78
|
+
}),
|
|
79
|
+
payload: { results: undefined },
|
|
80
|
+
error: undefined
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const results = await service.searchByPostcode('NW1 6XE', 'apikey')
|
|
84
|
+
|
|
85
|
+
expect(results).toHaveLength(0)
|
|
86
|
+
expect(results).toEqual([])
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('searchByUPRN', () => {
|
|
91
|
+
it('should return formatted addresses', async () => {
|
|
92
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
93
|
+
res: /** @type {IncomingMessage} */ ({
|
|
94
|
+
statusCode: 200,
|
|
95
|
+
headers: {}
|
|
96
|
+
}),
|
|
97
|
+
payload: uprnResult,
|
|
98
|
+
error: undefined
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const results = await service.searchByUPRN('100023071949', 'apikey')
|
|
102
|
+
|
|
103
|
+
expect(results).toHaveLength(1)
|
|
104
|
+
expect(results.at(0)).toEqual({
|
|
105
|
+
address: 'SHERLOCK HOLMES MUSEUM, 221B, BAKER STREET, LONDON, NW1 6XE',
|
|
106
|
+
addressLine1: 'Sherlock Holmes Museum 221b',
|
|
107
|
+
addressLine2: 'Baker Street',
|
|
108
|
+
county: '',
|
|
109
|
+
formatted: 'Sherlock Holmes Museum 221b, Baker Street, London, NW1 6XE',
|
|
110
|
+
postcode: 'NW1 6XE',
|
|
111
|
+
town: 'London',
|
|
112
|
+
uprn: '100023071949'
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('searchByQuery', () => {
|
|
118
|
+
it('should return formatted addresses', async () => {
|
|
119
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
120
|
+
res: /** @type {IncomingMessage} */ ({
|
|
121
|
+
statusCode: 200,
|
|
122
|
+
headers: {}
|
|
123
|
+
}),
|
|
124
|
+
payload: queryResult,
|
|
125
|
+
error: undefined
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const results = await service.searchByQuery(
|
|
129
|
+
'Prime minister downing',
|
|
130
|
+
'apikey'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
expect(results).toHaveLength(5)
|
|
134
|
+
expect(results.at(0)).toEqual({
|
|
135
|
+
address: 'BAKER STREET COTTAGE, BAKER STREET, FROME, BA11 3BL',
|
|
136
|
+
addressLine1: 'Baker Street Cottage',
|
|
137
|
+
addressLine2: 'Baker Street',
|
|
138
|
+
town: 'Frome',
|
|
139
|
+
county: '',
|
|
140
|
+
formatted: 'Baker Street Cottage, Baker Street, Frome, BA11 3BL',
|
|
141
|
+
postcode: 'BA11 3BL',
|
|
142
|
+
uprn: '250034655'
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('search', () => {
|
|
148
|
+
it('should return formatted addresses', async () => {
|
|
149
|
+
jest.mocked(getJson).mockResolvedValueOnce({
|
|
150
|
+
res: /** @type {IncomingMessage} */ ({
|
|
151
|
+
statusCode: 200,
|
|
152
|
+
headers: {}
|
|
153
|
+
}),
|
|
154
|
+
payload: postcodeResult,
|
|
155
|
+
error: undefined
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const results = await service.search('NW1 6XE', 'Emilia', 'apikey')
|
|
159
|
+
|
|
160
|
+
expect(results).toHaveLength(1)
|
|
161
|
+
expect(results.at(0)).toEqual({
|
|
162
|
+
address: "EMILIA'S CRAFTED PASTA, 215, BAKER STREET, LONDON, NW1 6XE",
|
|
163
|
+
addressLine1: "Emilia's Crafted Pasta 215",
|
|
164
|
+
addressLine2: 'Baker Street',
|
|
165
|
+
county: '',
|
|
166
|
+
formatted: "Emilia's Crafted Pasta 215, Baker Street, London, NW1 6XE",
|
|
167
|
+
postcode: 'NW1 6XE',
|
|
168
|
+
town: 'London',
|
|
169
|
+
uprn: '10033619968'
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @import { IncomingMessage } from 'node:http'
|
|
177
|
+
*/
|