@defra-fish/sales-api-service 1.64.0-rc.7 → 1.64.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 +5 -5
- package/src/schema/authenticate.schema.js +21 -2
- package/src/server/routes/__tests__/authenticate-new.spec.js +22 -16
- package/src/server/routes/__tests__/authenticate.spec.js +243 -0
- package/src/server/routes/authenticate.js +79 -34
- package/src/services/__tests__/recurring-payments.service.spec.js +98 -11
- package/src/services/recurring-payments.service.js +16 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/sales-api-service",
|
|
3
|
-
"version": "1.64.0
|
|
3
|
+
"version": "1.64.0",
|
|
4
4
|
"description": "Rod Licensing Sales API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@defra-fish/business-rules-lib": "1.64.0
|
|
39
|
-
"@defra-fish/connectors-lib": "1.64.0
|
|
40
|
-
"@defra-fish/dynamics-lib": "1.64.0
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.64.0",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.64.0",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.64.0",
|
|
41
41
|
"@hapi/boom": "9.1.2",
|
|
42
42
|
"@hapi/hapi": "20.1.3",
|
|
43
43
|
"@hapi/inert": "6.0.3",
|
|
@@ -52,5 +52,5 @@
|
|
|
52
52
|
"moment-timezone": "0.5.34",
|
|
53
53
|
"uuid": "8.3.2"
|
|
54
54
|
},
|
|
55
|
-
"gitHead": "
|
|
55
|
+
"gitHead": "d6a3d30c7a20169fdf5afb1a93eb1d411fe96abf"
|
|
56
56
|
}
|
|
@@ -20,10 +20,29 @@ export const authenticateRenewalRequestQuerySchema = Joi.object({
|
|
|
20
20
|
}).label('authenticate-renewal-request-query')
|
|
21
21
|
|
|
22
22
|
export const authenticateRenewalResponseSchema = Joi.object({
|
|
23
|
-
permission: {
|
|
23
|
+
permission: Joi.object({
|
|
24
24
|
...finalisedPermissionSchemaContent,
|
|
25
25
|
licensee: contactResponseSchema,
|
|
26
26
|
concessions: concessionProofSchema,
|
|
27
27
|
permit: permitSchema
|
|
28
|
-
}
|
|
28
|
+
})
|
|
29
29
|
}).label('authenticate-renewal-response')
|
|
30
|
+
|
|
31
|
+
export const rcpAuthenticateRenewalResponseSchema = Joi.object({
|
|
32
|
+
permission: Joi.object({
|
|
33
|
+
...finalisedPermissionSchemaContent,
|
|
34
|
+
licensee: contactResponseSchema,
|
|
35
|
+
concessions: concessionProofSchema,
|
|
36
|
+
permit: permitSchema
|
|
37
|
+
}),
|
|
38
|
+
recurringPayment: Joi.object({
|
|
39
|
+
id: Joi.string().uuid().required(),
|
|
40
|
+
agreementId: Joi.string().required(),
|
|
41
|
+
status: Joi.alternatives().try(Joi.number(), Joi.string()).required(),
|
|
42
|
+
nextDueDate: Joi.date().required(),
|
|
43
|
+
cancelledDate: Joi.date().allow(null),
|
|
44
|
+
cancelledReason: Joi.string().allow(null),
|
|
45
|
+
endDate: Joi.date().required(),
|
|
46
|
+
lastDigitsCardNumbers: Joi.string().required()
|
|
47
|
+
}).optional()
|
|
48
|
+
}).label('rcp-authenticate-renewal-response')
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
import
|
|
2
|
-
import db from 'debug'
|
|
3
|
-
jest.mock('@defra-fish/dynamics-lib')
|
|
4
|
-
jest.mock('debug')
|
|
1
|
+
import '@defra-fish/dynamics-lib'
|
|
5
2
|
|
|
6
3
|
describe('executeWithErrorLog', () => {
|
|
7
|
-
it('
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
it('logs the filter when executeQuery fails via the handler', async () => {
|
|
5
|
+
jest.resetModules()
|
|
6
|
+
|
|
7
|
+
const debugSpy = jest.fn()
|
|
8
|
+
jest.doMock('debug', () => jest.fn(() => debugSpy))
|
|
9
|
+
|
|
10
|
+
jest.doMock('@defra-fish/dynamics-lib', () => {
|
|
11
|
+
const actual = jest.requireActual('@defra-fish/dynamics-lib')
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
executeQuery: jest.fn().mockRejectedValueOnce(new Error('boom')),
|
|
15
|
+
contactForLicenseeNoReference: jest.fn(() => ({ filter: 'query filter test' })),
|
|
16
|
+
permissionForContacts: jest.fn(() => [])
|
|
17
|
+
}
|
|
12
18
|
})
|
|
13
|
-
|
|
14
|
-
const authenticate =
|
|
19
|
+
|
|
20
|
+
const authenticate = (await import('../authenticate.js')).default
|
|
15
21
|
const [
|
|
16
22
|
{
|
|
17
23
|
options: { handler }
|
|
18
24
|
}
|
|
19
25
|
] = authenticate
|
|
20
|
-
|
|
26
|
+
|
|
27
|
+
const request = {
|
|
21
28
|
query: { licenseeBirthDate: '', licenseePostcode: '' },
|
|
22
29
|
params: { referenceNumber: '' }
|
|
23
30
|
}
|
|
31
|
+
const h = { response: () => ({ code: () => {} }) }
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
await handler(mockRequest)
|
|
27
|
-
} catch {}
|
|
33
|
+
await handler(request, h).catch(() => {})
|
|
28
34
|
|
|
29
|
-
expect(
|
|
35
|
+
expect(debugSpy).toHaveBeenCalledWith('Error executing query with filter query filter test')
|
|
30
36
|
})
|
|
31
37
|
})
|
|
@@ -7,6 +7,14 @@ import {
|
|
|
7
7
|
MOCK_CONCESSION_PROOF_ENTITY,
|
|
8
8
|
MOCK_CONCESSION
|
|
9
9
|
} from '../../../__mocks__/test-data.js'
|
|
10
|
+
import authenticate from '../authenticate.js'
|
|
11
|
+
import { findLinkedRecurringPayment } from '../../../services/recurring-payments.service.js'
|
|
12
|
+
|
|
13
|
+
const [
|
|
14
|
+
{
|
|
15
|
+
options: { handler }
|
|
16
|
+
}
|
|
17
|
+
] = authenticate
|
|
10
18
|
|
|
11
19
|
jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
12
20
|
...jest.requireActual('@defra-fish/dynamics-lib'),
|
|
@@ -15,6 +23,10 @@ jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
|
15
23
|
permissionForContacts: jest.fn()
|
|
16
24
|
}))
|
|
17
25
|
|
|
26
|
+
jest.mock('../../../services/recurring-payments.service.js', () => ({
|
|
27
|
+
findLinkedRecurringPayment: jest.fn()
|
|
28
|
+
}))
|
|
29
|
+
|
|
18
30
|
let server = null
|
|
19
31
|
|
|
20
32
|
describe('authenticate handler', () => {
|
|
@@ -26,6 +38,10 @@ describe('authenticate handler', () => {
|
|
|
26
38
|
await server.stop()
|
|
27
39
|
})
|
|
28
40
|
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
jest.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
29
45
|
describe('authenticateRenewal', () => {
|
|
30
46
|
it('authenticates a renewal request', async () => {
|
|
31
47
|
executeQuery.mockResolvedValueOnce([
|
|
@@ -144,6 +160,7 @@ describe('authenticate handler', () => {
|
|
|
144
160
|
statusCode: 500
|
|
145
161
|
})
|
|
146
162
|
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
163
|
+
consoleErrorSpy.mockRestore()
|
|
147
164
|
})
|
|
148
165
|
|
|
149
166
|
it('throws 401 errors if the renewal could not be authenticated', async () => {
|
|
@@ -190,4 +207,230 @@ describe('authenticate handler', () => {
|
|
|
190
207
|
})
|
|
191
208
|
})
|
|
192
209
|
})
|
|
210
|
+
|
|
211
|
+
describe('authenticateRecurringPayment', () => {
|
|
212
|
+
const baseUrl = '/authenticate/rcp/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
|
|
213
|
+
|
|
214
|
+
it('authenticates a recurring payment request and returns recurringPayment', async () => {
|
|
215
|
+
executeQuery
|
|
216
|
+
.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }])
|
|
217
|
+
.mockResolvedValueOnce([
|
|
218
|
+
{
|
|
219
|
+
entity: MOCK_EXISTING_PERMISSION_ENTITY,
|
|
220
|
+
expanded: {
|
|
221
|
+
licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
|
|
222
|
+
concessionProofs: [{ entity: MOCK_CONCESSION_PROOF_ENTITY, expanded: { concession: { entity: MOCK_CONCESSION } } }],
|
|
223
|
+
permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
])
|
|
227
|
+
.mockResolvedValueOnce([{ entity: MOCK_CONCESSION_PROOF_ENTITY, expanded: { concession: { entity: MOCK_CONCESSION } } }])
|
|
228
|
+
|
|
229
|
+
findLinkedRecurringPayment.mockResolvedValueOnce({
|
|
230
|
+
id: 'rcp-123',
|
|
231
|
+
status: 1
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const result = await server.inject({ method: 'GET', url: baseUrl })
|
|
235
|
+
const body = JSON.parse(result.payload)
|
|
236
|
+
|
|
237
|
+
expect({
|
|
238
|
+
statusCode: result.statusCode,
|
|
239
|
+
body
|
|
240
|
+
}).toMatchObject({
|
|
241
|
+
statusCode: 200,
|
|
242
|
+
body: {
|
|
243
|
+
permission: expect.objectContaining({
|
|
244
|
+
...MOCK_EXISTING_PERMISSION_ENTITY.toJSON(),
|
|
245
|
+
licensee: MOCK_EXISTING_CONTACT_ENTITY.toJSON(),
|
|
246
|
+
concessions: [
|
|
247
|
+
{
|
|
248
|
+
id: MOCK_CONCESSION.id,
|
|
249
|
+
proof: MOCK_CONCESSION_PROOF_ENTITY.toJSON()
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
permit: MOCK_1DAY_SENIOR_PERMIT_ENTITY.toJSON()
|
|
253
|
+
}),
|
|
254
|
+
recurringPayment: expect.objectContaining({ id: 'rcp-123', status: 1 })
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('calls findLinkedRecurringPayment with permission id', async () => {
|
|
260
|
+
executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
|
|
261
|
+
{
|
|
262
|
+
entity: MOCK_EXISTING_PERMISSION_ENTITY,
|
|
263
|
+
expanded: {
|
|
264
|
+
licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
|
|
265
|
+
concessionProofs: [],
|
|
266
|
+
permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
findLinkedRecurringPayment.mockResolvedValueOnce({ id: 'rcp-123' })
|
|
272
|
+
|
|
273
|
+
await server.inject({ method: 'GET', url: baseUrl })
|
|
274
|
+
|
|
275
|
+
expect(findLinkedRecurringPayment).toHaveBeenCalledWith(MOCK_EXISTING_PERMISSION_ENTITY.id)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('returns 401 when no contacts found', async () => {
|
|
279
|
+
executeQuery.mockResolvedValueOnce([])
|
|
280
|
+
|
|
281
|
+
const result = await server.inject({ method: 'GET', url: baseUrl })
|
|
282
|
+
const body = JSON.parse(result.payload)
|
|
283
|
+
|
|
284
|
+
expect({
|
|
285
|
+
statusCode: result.statusCode,
|
|
286
|
+
body
|
|
287
|
+
}).toMatchObject({
|
|
288
|
+
statusCode: 401,
|
|
289
|
+
body: {
|
|
290
|
+
error: 'Unauthorized',
|
|
291
|
+
message: 'The licensee could not be authenticated'
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('returns 401 when no permissions match', async () => {
|
|
297
|
+
executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([])
|
|
298
|
+
|
|
299
|
+
const result = await server.inject({ method: 'GET', url: baseUrl })
|
|
300
|
+
const body = JSON.parse(result.payload)
|
|
301
|
+
|
|
302
|
+
expect({
|
|
303
|
+
statusCode: result.statusCode,
|
|
304
|
+
body
|
|
305
|
+
}).toMatchObject({
|
|
306
|
+
statusCode: 401,
|
|
307
|
+
body: {
|
|
308
|
+
error: 'Unauthorized',
|
|
309
|
+
message: 'The licensee could not be authenticated'
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('returns 500 when multiple permissions match', async () => {
|
|
315
|
+
executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
|
|
316
|
+
{
|
|
317
|
+
entity: { id: 'p1', referenceNumber: 'CD379B' },
|
|
318
|
+
expanded: { concessionProofs: [], licensee: { entity: {}, expanded: {} }, permit: { entity: {}, expanded: {} } }
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
entity: { id: 'p2', referenceNumber: 'CD379B' },
|
|
322
|
+
expanded: { concessionProofs: [], licensee: { entity: {}, expanded: {} }, permit: { entity: {}, expanded: {} } }
|
|
323
|
+
}
|
|
324
|
+
])
|
|
325
|
+
|
|
326
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
327
|
+
|
|
328
|
+
const result = await server.inject({ method: 'GET', url: baseUrl })
|
|
329
|
+
const body = JSON.parse(result.payload)
|
|
330
|
+
|
|
331
|
+
expect({
|
|
332
|
+
statusCode: result.statusCode,
|
|
333
|
+
body
|
|
334
|
+
}).toMatchObject({
|
|
335
|
+
statusCode: 500,
|
|
336
|
+
body: {
|
|
337
|
+
error: 'Internal Server Error',
|
|
338
|
+
message: 'Unable to authenticate, non-unique results for query'
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
expect(consoleErrorSpy).toHaveBeenCalled()
|
|
342
|
+
consoleErrorSpy.mockRestore()
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('returns 400 when query params are missing', async () => {
|
|
346
|
+
const result = await server.inject({ method: 'GET', url: '/authenticate/rcp/CD379B?' })
|
|
347
|
+
const body = JSON.parse(result.payload)
|
|
348
|
+
|
|
349
|
+
expect({
|
|
350
|
+
statusCode: result.statusCode,
|
|
351
|
+
body
|
|
352
|
+
}).toMatchObject({
|
|
353
|
+
statusCode: 400,
|
|
354
|
+
body: {
|
|
355
|
+
error: 'Bad Request',
|
|
356
|
+
message: 'Invalid query: "licenseeBirthDate" is required'
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('if no concessions are returned', () => {
|
|
362
|
+
it('returns permission and recurringPayment without concessions', async () => {
|
|
363
|
+
executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
|
|
364
|
+
{
|
|
365
|
+
entity: MOCK_EXISTING_PERMISSION_ENTITY,
|
|
366
|
+
expanded: {
|
|
367
|
+
licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
|
|
368
|
+
concessionProofs: [],
|
|
369
|
+
permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
])
|
|
373
|
+
|
|
374
|
+
findLinkedRecurringPayment.mockResolvedValueOnce({
|
|
375
|
+
id: 'rcp-789',
|
|
376
|
+
status: 1
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const result = await server.inject({ method: 'GET', url: baseUrl })
|
|
380
|
+
const body = JSON.parse(result.payload)
|
|
381
|
+
|
|
382
|
+
expect({
|
|
383
|
+
statusCode: result.statusCode,
|
|
384
|
+
body
|
|
385
|
+
}).toMatchObject({
|
|
386
|
+
statusCode: 200,
|
|
387
|
+
body: {
|
|
388
|
+
permission: expect.objectContaining({
|
|
389
|
+
...MOCK_EXISTING_PERMISSION_ENTITY.toJSON(),
|
|
390
|
+
licensee: MOCK_EXISTING_CONTACT_ENTITY.toJSON(),
|
|
391
|
+
concessions: [],
|
|
392
|
+
permit: MOCK_1DAY_SENIOR_PERMIT_ENTITY.toJSON()
|
|
393
|
+
}),
|
|
394
|
+
recurringPayment: expect.objectContaining({ id: 'rcp-789', status: 1 })
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('changes reference number to uppercase', async () => {
|
|
402
|
+
const sampleQueryReferenceNumber = 'abc123'
|
|
403
|
+
const sampleResultReferenceNumber = sampleQueryReferenceNumber.toUpperCase()
|
|
404
|
+
const makeMockEntity = (obj = {}) => ({
|
|
405
|
+
...obj,
|
|
406
|
+
toJSON: () => obj
|
|
407
|
+
})
|
|
408
|
+
executeQuery.mockReturnValueOnce([{ entity: { id: 'hgk-999' } }]).mockReturnValueOnce([
|
|
409
|
+
{
|
|
410
|
+
entity: makeMockEntity({
|
|
411
|
+
referenceNumber: sampleResultReferenceNumber
|
|
412
|
+
}),
|
|
413
|
+
expanded: {
|
|
414
|
+
concessionProofs: [],
|
|
415
|
+
licensee: { entity: makeMockEntity() },
|
|
416
|
+
permit: { entity: makeMockEntity() }
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
])
|
|
420
|
+
const mockRequest = {
|
|
421
|
+
query: { licenseeBirthDate: '', licenseePostcode: '' },
|
|
422
|
+
params: { referenceNumber: sampleQueryReferenceNumber }
|
|
423
|
+
}
|
|
424
|
+
const mockResponseToolkit = { response: jest.fn(() => ({ code: () => {} })) }
|
|
425
|
+
|
|
426
|
+
await handler(mockRequest, mockResponseToolkit)
|
|
427
|
+
|
|
428
|
+
expect(mockResponseToolkit.response).toHaveBeenCalledWith(
|
|
429
|
+
expect.objectContaining({
|
|
430
|
+
permission: expect.objectContaining({
|
|
431
|
+
referenceNumber: sampleResultReferenceNumber
|
|
432
|
+
})
|
|
433
|
+
})
|
|
434
|
+
)
|
|
435
|
+
})
|
|
193
436
|
})
|
|
@@ -2,12 +2,16 @@ import Boom from '@hapi/boom'
|
|
|
2
2
|
import {
|
|
3
3
|
authenticateRenewalRequestParamsSchema,
|
|
4
4
|
authenticateRenewalRequestQuerySchema,
|
|
5
|
-
authenticateRenewalResponseSchema
|
|
5
|
+
authenticateRenewalResponseSchema,
|
|
6
|
+
rcpAuthenticateRenewalResponseSchema
|
|
6
7
|
} from '../../schema/authenticate.schema.js'
|
|
7
8
|
import db from 'debug'
|
|
8
9
|
import { permissionForContacts, concessionsByIds, executeQuery, contactForLicenseeNoReference } from '@defra-fish/dynamics-lib'
|
|
10
|
+
import { findLinkedRecurringPayment } from '../../services/recurring-payments.service.js'
|
|
11
|
+
|
|
9
12
|
const debug = db('sales:renewal-authentication')
|
|
10
13
|
const failAuthenticate = 'The licensee could not be authenticated'
|
|
14
|
+
const HTTP_OK = 200
|
|
11
15
|
|
|
12
16
|
const executeWithErrorLog = async query => {
|
|
13
17
|
try {
|
|
@@ -18,45 +22,51 @@ const executeWithErrorLog = async query => {
|
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
const getAuthenticatedPermission = async request => {
|
|
26
|
+
const { licenseeBirthDate, licenseePostcode } = request.query
|
|
27
|
+
const contacts = await executeWithErrorLog(contactForLicenseeNoReference(licenseeBirthDate, licenseePostcode))
|
|
28
|
+
|
|
29
|
+
if (!contacts.length) {
|
|
30
|
+
throw Boom.unauthorized(failAuthenticate)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const contactIds = contacts.map(contact => contact.entity.id)
|
|
34
|
+
const permissions = await executeWithErrorLog(permissionForContacts(contactIds))
|
|
35
|
+
const results = permissions.filter(p => p.entity.referenceNumber.endsWith(request.params.referenceNumber.toUpperCase()))
|
|
36
|
+
|
|
37
|
+
if (results.length === 0) {
|
|
38
|
+
throw Boom.unauthorized(failAuthenticate)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (results.length > 1) {
|
|
42
|
+
throw new Error('Unable to authenticate, non-unique results for query')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [permission] = results
|
|
46
|
+
const concessionIds = permission.expanded.concessionProofs.map(f => f.entity.id)
|
|
47
|
+
const concessionProofs = permission.expanded.concessionProofs.length ? await executeWithErrorLog(concessionsByIds(concessionIds)) : []
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
permission: {
|
|
51
|
+
...permission.entity.toJSON(),
|
|
52
|
+
licensee: permission.expanded.licensee.entity.toJSON(),
|
|
53
|
+
concessions: concessionProofs.map(c => ({
|
|
54
|
+
id: c.expanded.concession.entity.id,
|
|
55
|
+
proof: c.entity.toJSON()
|
|
56
|
+
})),
|
|
57
|
+
permit: permission.expanded.permit.entity.toJSON()
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
21
62
|
export default [
|
|
22
63
|
{
|
|
23
64
|
method: 'GET',
|
|
24
65
|
path: '/authenticate/renewal/{referenceNumber}',
|
|
25
66
|
options: {
|
|
26
67
|
handler: async (request, h) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (contacts.length > 0) {
|
|
30
|
-
const contactIds = contacts.map(contact => contact.entity.id)
|
|
31
|
-
const permissions = await executeWithErrorLog(permissionForContacts(contactIds))
|
|
32
|
-
const results = permissions.filter(p => p.entity.referenceNumber.endsWith(request.params.referenceNumber))
|
|
33
|
-
if (results.length === 1) {
|
|
34
|
-
let concessionProofs = []
|
|
35
|
-
if (results[0].expanded.concessionProofs.length > 0) {
|
|
36
|
-
const ids = results[0].expanded.concessionProofs.map(f => f.entity.id)
|
|
37
|
-
concessionProofs = await executeWithErrorLog(concessionsByIds(ids))
|
|
38
|
-
}
|
|
39
|
-
return h
|
|
40
|
-
.response({
|
|
41
|
-
permission: {
|
|
42
|
-
...results[0].entity.toJSON(),
|
|
43
|
-
licensee: results[0].expanded.licensee.entity.toJSON(),
|
|
44
|
-
concessions: concessionProofs.map(c => ({
|
|
45
|
-
id: c.expanded.concession.entity.id,
|
|
46
|
-
proof: c.entity.toJSON()
|
|
47
|
-
})),
|
|
48
|
-
permit: results[0].expanded.permit.entity.toJSON()
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
.code(200)
|
|
52
|
-
} else if (results.length === 0) {
|
|
53
|
-
throw Boom.unauthorized(failAuthenticate)
|
|
54
|
-
} else {
|
|
55
|
-
throw new Error('Unable to authenticate, non-unique results for query')
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
throw Boom.unauthorized(failAuthenticate)
|
|
59
|
-
}
|
|
68
|
+
const permissionData = await getAuthenticatedPermission(request)
|
|
69
|
+
return h.response(permissionData).code(HTTP_OK)
|
|
60
70
|
},
|
|
61
71
|
description: 'Authenticate a licensee by checking the licence number corresponds with the provided contact details',
|
|
62
72
|
notes: `
|
|
@@ -77,5 +87,40 @@ export default [
|
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
method: 'GET',
|
|
93
|
+
path: '/authenticate/rcp/{referenceNumber}',
|
|
94
|
+
options: {
|
|
95
|
+
handler: async (request, h) => {
|
|
96
|
+
const { permission } = await getAuthenticatedPermission(request)
|
|
97
|
+
const recurringPayment = await findLinkedRecurringPayment(permission.id)
|
|
98
|
+
return h.response({ permission, recurringPayment }).code(HTTP_OK)
|
|
99
|
+
},
|
|
100
|
+
description:
|
|
101
|
+
'Authenticate a licensee by checking the licence number corresponds with the provided contact details. Checking agreement id exists and recurring payment is active and not cancelled',
|
|
102
|
+
notes: `
|
|
103
|
+
Authenticate a licensee by checking the licence number corresponds with the provided contact details. Checking agreement id exists and recurring payment is active and not cancelled
|
|
104
|
+
`,
|
|
105
|
+
tags: ['api', 'authenticate'],
|
|
106
|
+
validate: {
|
|
107
|
+
params: authenticateRenewalRequestParamsSchema,
|
|
108
|
+
query: authenticateRenewalRequestQuerySchema
|
|
109
|
+
},
|
|
110
|
+
plugins: {
|
|
111
|
+
'hapi-swagger': {
|
|
112
|
+
responses: {
|
|
113
|
+
200: {
|
|
114
|
+
description: 'The licensee was successfully authenticated',
|
|
115
|
+
schema: rcpAuthenticateRenewalResponseSchema
|
|
116
|
+
},
|
|
117
|
+
401: { description: failAuthenticate }
|
|
118
|
+
},
|
|
119
|
+
order: 2
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
80
123
|
}
|
|
81
124
|
]
|
|
125
|
+
|
|
126
|
+
export const errorLogTest = { executeWithErrorLog }
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
findById,
|
|
7
7
|
Permission,
|
|
8
8
|
persist,
|
|
9
|
-
RecurringPayment
|
|
9
|
+
RecurringPayment,
|
|
10
|
+
findRecurringPaymentByPermissionId,
|
|
11
|
+
retrieveGlobalOptionSets
|
|
10
12
|
} from '@defra-fish/dynamics-lib'
|
|
11
13
|
import {
|
|
12
14
|
getRecurringPayments,
|
|
@@ -15,7 +17,8 @@ import {
|
|
|
15
17
|
processRPResult,
|
|
16
18
|
findNewestExistingRecurringPaymentInCrm,
|
|
17
19
|
getRecurringPaymentAgreement,
|
|
18
|
-
cancelRecurringPayment
|
|
20
|
+
cancelRecurringPayment,
|
|
21
|
+
findLinkedRecurringPayment
|
|
19
22
|
} from '../recurring-payments.service.js'
|
|
20
23
|
import { calculateEndDate, generatePermissionNumber } from '../permissions.service.js'
|
|
21
24
|
import { getObfuscatedDob } from '../contacts.service.js'
|
|
@@ -50,7 +53,9 @@ jest.mock('@defra-fish/dynamics-lib', () => ({
|
|
|
50
53
|
dynamicsClient: {
|
|
51
54
|
retrieveMultipleRequest: jest.fn(() => ({ value: [] }))
|
|
52
55
|
},
|
|
53
|
-
persist: jest.fn()
|
|
56
|
+
persist: jest.fn(),
|
|
57
|
+
findRecurringPaymentByPermissionId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
|
|
58
|
+
retrieveGlobalOptionSets: jest.fn()
|
|
54
59
|
}))
|
|
55
60
|
|
|
56
61
|
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
@@ -720,8 +725,8 @@ describe('recurring payments service', () => {
|
|
|
720
725
|
status: expect.objectContaining({ id: TRANSACTION_STATUS.FINALISED }),
|
|
721
726
|
payment: expect.objectContaining({
|
|
722
727
|
amount: mockTransaction.cost,
|
|
723
|
-
|
|
724
|
-
|
|
728
|
+
source: TRANSACTION_SOURCE.govPay,
|
|
729
|
+
method: PAYMENT_TYPE.debit,
|
|
725
730
|
timestamp: fakeNow
|
|
726
731
|
})
|
|
727
732
|
})
|
|
@@ -776,8 +781,8 @@ describe('recurring payments service', () => {
|
|
|
776
781
|
it('returns a Recurring Payment (not a plain object)', async () => {
|
|
777
782
|
jest.spyOn(RecurringPayment, 'fromResponse')
|
|
778
783
|
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce(getMockResponse())
|
|
779
|
-
const
|
|
780
|
-
expect(RecurringPayment.fromResponse.mock.results[0].value).toBe(
|
|
784
|
+
const recurringPayment = await findNewestExistingRecurringPaymentInCrm()
|
|
785
|
+
expect(RecurringPayment.fromResponse.mock.results[0].value).toBe(recurringPayment)
|
|
781
786
|
})
|
|
782
787
|
|
|
783
788
|
it.each([
|
|
@@ -811,14 +816,14 @@ describe('recurring payments service', () => {
|
|
|
811
816
|
]
|
|
812
817
|
])('returns most recent existing recurring payment from %s', async (_desc, mockResponseData, expectedId) => {
|
|
813
818
|
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce({ value: mockResponseData })
|
|
814
|
-
const
|
|
815
|
-
expect(
|
|
819
|
+
const recurringPayment = await findNewestExistingRecurringPaymentInCrm()
|
|
820
|
+
expect(recurringPayment.id).toBe(expectedId)
|
|
816
821
|
})
|
|
817
822
|
|
|
818
823
|
it('returns boolean false if no recurring payments found', async () => {
|
|
819
824
|
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce({ value: [] })
|
|
820
|
-
const
|
|
821
|
-
expect(
|
|
825
|
+
const recurringPayment = await findNewestExistingRecurringPaymentInCrm()
|
|
826
|
+
expect(recurringPayment).toBeFalsy()
|
|
822
827
|
})
|
|
823
828
|
})
|
|
824
829
|
|
|
@@ -882,6 +887,7 @@ describe('recurring payments service', () => {
|
|
|
882
887
|
|
|
883
888
|
describe('cancelRecurringPayment', () => {
|
|
884
889
|
it('should call findById with RecurringPayment and the provided id', async () => {
|
|
890
|
+
retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ definition: 'mock-def' }) })
|
|
885
891
|
findById.mockReturnValueOnce(getMockRecurringPayment())
|
|
886
892
|
const id = 'abc123'
|
|
887
893
|
await cancelRecurringPayment(id)
|
|
@@ -889,6 +895,20 @@ describe('recurring payments service', () => {
|
|
|
889
895
|
})
|
|
890
896
|
|
|
891
897
|
it('should call persist with the updated RecurringPayment', async () => {
|
|
898
|
+
retrieveGlobalOptionSets.mockReturnValueOnce({
|
|
899
|
+
cached: jest.fn().mockResolvedValue({
|
|
900
|
+
defra_cancelledreasons: {
|
|
901
|
+
options: {
|
|
902
|
+
910400002: {
|
|
903
|
+
id: 910400002,
|
|
904
|
+
label: 'Payment Failure',
|
|
905
|
+
description: 'Payment Failure'
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
})
|
|
910
|
+
})
|
|
911
|
+
|
|
892
912
|
const recurringPayment = getMockRecurringPayment()
|
|
893
913
|
findById.mockReturnValueOnce(recurringPayment)
|
|
894
914
|
|
|
@@ -907,4 +927,71 @@ describe('recurring payments service', () => {
|
|
|
907
927
|
await expect(cancelRecurringPayment('id')).rejects.toThrow('Invalid id provided for recurring payment cancellation')
|
|
908
928
|
})
|
|
909
929
|
})
|
|
930
|
+
|
|
931
|
+
describe('findLinkedRecurringPayment', () => {
|
|
932
|
+
const arrangeLinkedRcpSuccess = mockResponse => {
|
|
933
|
+
jest.spyOn(RecurringPayment, 'fromResponse')
|
|
934
|
+
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce(mockResponse)
|
|
935
|
+
retrieveGlobalOptionSets.mockReturnValueOnce({
|
|
936
|
+
cached: jest.fn().mockResolvedValue({ definition: 'mock-def' })
|
|
937
|
+
})
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
it('passes permission id to findRecurringPaymentByPermissionId', async () => {
|
|
941
|
+
const permissionId = Symbol('permission-id')
|
|
942
|
+
await findLinkedRecurringPayment(permissionId)
|
|
943
|
+
expect(findRecurringPaymentByPermissionId).toHaveBeenCalledWith(permissionId)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
it('passes query created by findRecurringPaymentByPermissionId to retrieveMultipleRequest', async () => {
|
|
947
|
+
const retrieveRequest = Symbol('retrieve request')
|
|
948
|
+
findRecurringPaymentByPermissionId.mockReturnValueOnce({ toRetrieveRequest: () => retrieveRequest })
|
|
949
|
+
await findLinkedRecurringPayment()
|
|
950
|
+
expect(dynamicsClient.retrieveMultipleRequest).toHaveBeenCalledWith(retrieveRequest)
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
it('calls RecurringPayment.fromResponse with response and definitions', async () => {
|
|
954
|
+
arrangeLinkedRcpSuccess(getMockResponse())
|
|
955
|
+
await findLinkedRecurringPayment('abc123')
|
|
956
|
+
expect(RecurringPayment.fromResponse).toHaveBeenCalledWith(expect.any(Object), expect.anything())
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
it('returns the RecurringPayment produced by fromResponse', async () => {
|
|
960
|
+
arrangeLinkedRcpSuccess(getMockResponse())
|
|
961
|
+
const recurringPayment = await findLinkedRecurringPayment('abc123')
|
|
962
|
+
expect(RecurringPayment.fromResponse.mock.results[0].value).toBe(recurringPayment)
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
it.each([
|
|
966
|
+
[
|
|
967
|
+
'two with last most recent',
|
|
968
|
+
[
|
|
969
|
+
{ defra_recurringpaymentid: 'rcp-123', defra_enddate: '2024-01-01T00:00:00Z' },
|
|
970
|
+
{ defra_recurringpaymentid: 'rcp-234', defra_enddate: '2025-01-01T00:00:00Z' }
|
|
971
|
+
],
|
|
972
|
+
'rcp-234'
|
|
973
|
+
],
|
|
974
|
+
[
|
|
975
|
+
'three with middle most recent',
|
|
976
|
+
[
|
|
977
|
+
{ defra_recurringpaymentid: 'rcp-345', defra_enddate: '2023-01-01T00:00:00Z' },
|
|
978
|
+
{ defra_recurringpaymentid: 'rcp-456', defra_enddate: '2026-01-01T00:00:00Z' },
|
|
979
|
+
{ defra_recurringpaymentid: 'rcp-567', defra_enddate: '2025-01-01T00:00:00Z' }
|
|
980
|
+
],
|
|
981
|
+
'rcp-456'
|
|
982
|
+
]
|
|
983
|
+
])('returns the most recent linked recurring payment (%s)', async (_desc, mockData, expectedId) => {
|
|
984
|
+
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce({ value: mockData })
|
|
985
|
+
retrieveGlobalOptionSets.mockReturnValueOnce({ cached: jest.fn().mockResolvedValue({ def: 'mock' }) })
|
|
986
|
+
|
|
987
|
+
const recurringPayment = await findLinkedRecurringPayment('abc123')
|
|
988
|
+
expect(recurringPayment.id).toBe(expectedId)
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
it('returns false if no linked recurring payments found', async () => {
|
|
992
|
+
dynamicsClient.retrieveMultipleRequest.mockReturnValueOnce({ value: [] })
|
|
993
|
+
const recurringPayment = await findLinkedRecurringPayment('abc123')
|
|
994
|
+
expect(recurringPayment).toBeFalsy()
|
|
995
|
+
})
|
|
996
|
+
})
|
|
910
997
|
})
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
findDueRecurringPayments,
|
|
6
6
|
findRecurringPaymentsByAgreementId,
|
|
7
7
|
persist,
|
|
8
|
-
RecurringPayment
|
|
8
|
+
RecurringPayment,
|
|
9
|
+
findRecurringPaymentByPermissionId,
|
|
10
|
+
retrieveGlobalOptionSets
|
|
9
11
|
} from '@defra-fish/dynamics-lib'
|
|
10
12
|
import { calculateEndDate, generatePermissionNumber } from './permissions.service.js'
|
|
11
13
|
import { getObfuscatedDob } from './contacts.service.js'
|
|
@@ -136,8 +138,8 @@ export const processRPResult = async (transactionId, paymentId, createdDate) =>
|
|
|
136
138
|
status: { id: TRANSACTION_STATUS.FINALISED },
|
|
137
139
|
payment: {
|
|
138
140
|
amount: transactionRecord.cost,
|
|
139
|
-
method:
|
|
140
|
-
source:
|
|
141
|
+
method: PAYMENT_TYPE.debit,
|
|
142
|
+
source: TRANSACTION_SOURCE.govPay,
|
|
141
143
|
timestamp: new Date().toISOString()
|
|
142
144
|
}
|
|
143
145
|
}),
|
|
@@ -182,3 +184,14 @@ const determineRecurringPaymentName = (transactionRecord, contact) => {
|
|
|
182
184
|
const [dueYear] = transactionRecord.payment.recurring.nextDueDate.split('-')
|
|
183
185
|
return [contact.firstName, contact.lastName, dueYear].join(' ')
|
|
184
186
|
}
|
|
187
|
+
|
|
188
|
+
export const findLinkedRecurringPayment = async permissionId => {
|
|
189
|
+
const query = findRecurringPaymentByPermissionId(permissionId)
|
|
190
|
+
const response = await dynamicsClient.retrieveMultipleRequest(query.toRetrieveRequest())
|
|
191
|
+
if (response.value.length) {
|
|
192
|
+
const [rcpResponseData] = response.value.sort((a, b) => Date.parse(b.defra_enddate) - Date.parse(a.defra_enddate))
|
|
193
|
+
const definition = await retrieveGlobalOptionSets().cached()
|
|
194
|
+
return RecurringPayment.fromResponse(rcpResponseData, definition)
|
|
195
|
+
}
|
|
196
|
+
return false
|
|
197
|
+
}
|