@defra-fish/sales-api-service 1.64.0-rc.8 → 1.65.0-rc.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-fish/sales-api-service",
3
- "version": "1.64.0-rc.8",
3
+ "version": "1.65.0-rc.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-rc.8",
39
- "@defra-fish/connectors-lib": "1.64.0-rc.8",
40
- "@defra-fish/dynamics-lib": "1.64.0-rc.8",
38
+ "@defra-fish/business-rules-lib": "1.65.0-rc.0",
39
+ "@defra-fish/connectors-lib": "1.65.0-rc.0",
40
+ "@defra-fish/dynamics-lib": "1.65.0-rc.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": "88a60cab95e119b46cec17336d784aec1aeabae8"
55
+ "gitHead": "cdf9253964871c4afe0ff85d085364ffe4e890cf"
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 { contactForLicenseeNoReference, executeQuery } from '@defra-fish/dynamics-lib'
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('throws error', async () => {
8
- const debug = jest.fn()
9
- db.mockReturnValueOnce(debug)
10
- executeQuery.mockImplementation(() => {
11
- throw new Error()
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
- contactForLicenseeNoReference.mockReturnValueOnce({ filter: 'query filter test' })
14
- const authenticate = require('../authenticate.js').default
19
+
20
+ const authenticate = (await import('../authenticate.js')).default
15
21
  const [
16
22
  {
17
23
  options: { handler }
18
24
  }
19
25
  ] = authenticate
20
- const mockRequest = {
26
+
27
+ const request = {
21
28
  query: { licenseeBirthDate: '', licenseePostcode: '' },
22
29
  params: { referenceNumber: '' }
23
30
  }
31
+ const h = { response: () => ({ code: () => {} }) }
24
32
 
25
- try {
26
- await handler(mockRequest)
27
- } catch {}
33
+ await handler(request, h).catch(() => {})
28
34
 
29
- expect(debug).toHaveBeenCalledWith('Error executing query with filter query filter test')
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,7 +38,13 @@ describe('authenticate handler', () => {
26
38
  await server.stop()
27
39
  })
28
40
 
41
+ beforeEach(() => {
42
+ jest.clearAllMocks()
43
+ })
44
+
29
45
  describe('authenticateRenewal', () => {
46
+ const baseUrl = '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB123CD'
47
+
30
48
  it('authenticates a renewal request', async () => {
31
49
  executeQuery.mockResolvedValueOnce([
32
50
  {
@@ -47,7 +65,7 @@ describe('authenticate handler', () => {
47
65
  executeQuery.mockResolvedValueOnce([{ entity: MOCK_CONCESSION_PROOF_ENTITY, expanded: { concession: { entity: MOCK_CONCESSION } } }])
48
66
  const result = await server.inject({
49
67
  method: 'GET',
50
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
68
+ url: baseUrl
51
69
  })
52
70
  expect(result.statusCode).toBe(200)
53
71
  expect(JSON.parse(result.payload)).toMatchObject({
@@ -88,7 +106,7 @@ describe('authenticate handler', () => {
88
106
  it('should call contactForLicenseeNoReference with dob and postcode for a renewal request', async () => {
89
107
  await server.inject({
90
108
  method: 'GET',
91
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
109
+ url: baseUrl
92
110
  })
93
111
  expect(contactForLicenseeNoReference).toHaveBeenCalledWith('2000-01-01', 'AB12 3CD')
94
112
  })
@@ -96,7 +114,7 @@ describe('authenticate handler', () => {
96
114
  it('should call permissionForContacts with contact ids from contactForLicenseeNoReference', async () => {
97
115
  await server.inject({
98
116
  method: 'GET',
99
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
117
+ url: baseUrl
100
118
  })
101
119
  expect(permissionForContacts).toHaveBeenCalledWith([MOCK_EXISTING_CONTACT_ENTITY.id])
102
120
  })
@@ -104,7 +122,7 @@ describe('authenticate handler', () => {
104
122
  it('returns 200 from a renewal request', async () => {
105
123
  const result = await server.inject({
106
124
  method: 'GET',
107
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
125
+ url: baseUrl
108
126
  })
109
127
  expect(result.statusCode).toBe(200)
110
128
  })
@@ -112,7 +130,7 @@ describe('authenticate handler', () => {
112
130
  it('returns permission from a renewal request', async () => {
113
131
  const result = await server.inject({
114
132
  method: 'GET',
115
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
133
+ url: baseUrl
116
134
  })
117
135
  expect(JSON.parse(result.payload)).toMatchObject({
118
136
  permission: expect.objectContaining({
@@ -135,7 +153,7 @@ describe('authenticate handler', () => {
135
153
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
136
154
  const result = await server.inject({
137
155
  method: 'GET',
138
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
156
+ url: baseUrl
139
157
  })
140
158
  expect(result.statusCode).toBe(500)
141
159
  expect(JSON.parse(result.payload)).toMatchObject({
@@ -144,6 +162,7 @@ describe('authenticate handler', () => {
144
162
  statusCode: 500
145
163
  })
146
164
  expect(consoleErrorSpy).toHaveBeenCalled()
165
+ consoleErrorSpy.mockRestore()
147
166
  })
148
167
 
149
168
  it('throws 401 errors if the renewal could not be authenticated', async () => {
@@ -156,7 +175,7 @@ describe('authenticate handler', () => {
156
175
  executeQuery.mockResolvedValueOnce([])
157
176
  const result = await server.inject({
158
177
  method: 'GET',
159
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
178
+ url: baseUrl
160
179
  })
161
180
  expect(result.statusCode).toBe(401)
162
181
  expect(JSON.parse(result.payload)).toMatchObject({
@@ -170,7 +189,7 @@ describe('authenticate handler', () => {
170
189
  executeQuery.mockResolvedValueOnce([])
171
190
  const result = await server.inject({
172
191
  method: 'GET',
173
- url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
192
+ url: baseUrl
174
193
  })
175
194
  expect(result.statusCode).toBe(401)
176
195
  expect(JSON.parse(result.payload)).toMatchObject({
@@ -190,4 +209,230 @@ describe('authenticate handler', () => {
190
209
  })
191
210
  })
192
211
  })
212
+
213
+ describe('authenticateRecurringPayment', () => {
214
+ const baseUrl = '/authenticate/rcp/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
215
+
216
+ it('authenticates a recurring payment request and returns recurringPayment', async () => {
217
+ executeQuery
218
+ .mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }])
219
+ .mockResolvedValueOnce([
220
+ {
221
+ entity: MOCK_EXISTING_PERMISSION_ENTITY,
222
+ expanded: {
223
+ licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
224
+ concessionProofs: [{ entity: MOCK_CONCESSION_PROOF_ENTITY, expanded: { concession: { entity: MOCK_CONCESSION } } }],
225
+ permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
226
+ }
227
+ }
228
+ ])
229
+ .mockResolvedValueOnce([{ entity: MOCK_CONCESSION_PROOF_ENTITY, expanded: { concession: { entity: MOCK_CONCESSION } } }])
230
+
231
+ findLinkedRecurringPayment.mockResolvedValueOnce({
232
+ id: 'rcp-123',
233
+ status: 1
234
+ })
235
+
236
+ const result = await server.inject({ method: 'GET', url: baseUrl })
237
+ const body = JSON.parse(result.payload)
238
+
239
+ expect({
240
+ statusCode: result.statusCode,
241
+ body
242
+ }).toMatchObject({
243
+ statusCode: 200,
244
+ body: {
245
+ permission: expect.objectContaining({
246
+ ...MOCK_EXISTING_PERMISSION_ENTITY.toJSON(),
247
+ licensee: MOCK_EXISTING_CONTACT_ENTITY.toJSON(),
248
+ concessions: [
249
+ {
250
+ id: MOCK_CONCESSION.id,
251
+ proof: MOCK_CONCESSION_PROOF_ENTITY.toJSON()
252
+ }
253
+ ],
254
+ permit: MOCK_1DAY_SENIOR_PERMIT_ENTITY.toJSON()
255
+ }),
256
+ recurringPayment: expect.objectContaining({ id: 'rcp-123', status: 1 })
257
+ }
258
+ })
259
+ })
260
+
261
+ it('calls findLinkedRecurringPayment with permission id', async () => {
262
+ executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
263
+ {
264
+ entity: MOCK_EXISTING_PERMISSION_ENTITY,
265
+ expanded: {
266
+ licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
267
+ concessionProofs: [],
268
+ permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
269
+ }
270
+ }
271
+ ])
272
+
273
+ findLinkedRecurringPayment.mockResolvedValueOnce({ id: 'rcp-123' })
274
+
275
+ await server.inject({ method: 'GET', url: baseUrl })
276
+
277
+ expect(findLinkedRecurringPayment).toHaveBeenCalledWith(MOCK_EXISTING_PERMISSION_ENTITY.id)
278
+ })
279
+
280
+ it('returns 401 when no contacts found', async () => {
281
+ executeQuery.mockResolvedValueOnce([])
282
+
283
+ const result = await server.inject({ method: 'GET', url: baseUrl })
284
+ const body = JSON.parse(result.payload)
285
+
286
+ expect({
287
+ statusCode: result.statusCode,
288
+ body
289
+ }).toMatchObject({
290
+ statusCode: 401,
291
+ body: {
292
+ error: 'Unauthorized',
293
+ message: 'The licensee could not be authenticated'
294
+ }
295
+ })
296
+ })
297
+
298
+ it('returns 401 when no permissions match', async () => {
299
+ executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([])
300
+
301
+ const result = await server.inject({ method: 'GET', url: baseUrl })
302
+ const body = JSON.parse(result.payload)
303
+
304
+ expect({
305
+ statusCode: result.statusCode,
306
+ body
307
+ }).toMatchObject({
308
+ statusCode: 401,
309
+ body: {
310
+ error: 'Unauthorized',
311
+ message: 'The licensee could not be authenticated'
312
+ }
313
+ })
314
+ })
315
+
316
+ it('returns 500 when multiple permissions match', async () => {
317
+ executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
318
+ {
319
+ entity: { id: 'p1', referenceNumber: 'CD379B' },
320
+ expanded: { concessionProofs: [], licensee: { entity: {}, expanded: {} }, permit: { entity: {}, expanded: {} } }
321
+ },
322
+ {
323
+ entity: { id: 'p2', referenceNumber: 'CD379B' },
324
+ expanded: { concessionProofs: [], licensee: { entity: {}, expanded: {} }, permit: { entity: {}, expanded: {} } }
325
+ }
326
+ ])
327
+
328
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
329
+
330
+ const result = await server.inject({ method: 'GET', url: baseUrl })
331
+ const body = JSON.parse(result.payload)
332
+
333
+ expect({
334
+ statusCode: result.statusCode,
335
+ body
336
+ }).toMatchObject({
337
+ statusCode: 500,
338
+ body: {
339
+ error: 'Internal Server Error',
340
+ message: 'Unable to authenticate, non-unique results for query'
341
+ }
342
+ })
343
+ expect(consoleErrorSpy).toHaveBeenCalled()
344
+ consoleErrorSpy.mockRestore()
345
+ })
346
+
347
+ it('returns 400 when query params are missing', async () => {
348
+ const result = await server.inject({ method: 'GET', url: '/authenticate/rcp/CD379B?' })
349
+ const body = JSON.parse(result.payload)
350
+
351
+ expect({
352
+ statusCode: result.statusCode,
353
+ body
354
+ }).toMatchObject({
355
+ statusCode: 400,
356
+ body: {
357
+ error: 'Bad Request',
358
+ message: 'Invalid query: "licenseeBirthDate" is required'
359
+ }
360
+ })
361
+ })
362
+
363
+ describe('if no concessions are returned', () => {
364
+ it('returns permission and recurringPayment without concessions', async () => {
365
+ executeQuery.mockResolvedValueOnce([{ entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} }]).mockResolvedValueOnce([
366
+ {
367
+ entity: MOCK_EXISTING_PERMISSION_ENTITY,
368
+ expanded: {
369
+ licensee: { entity: MOCK_EXISTING_CONTACT_ENTITY, expanded: {} },
370
+ concessionProofs: [],
371
+ permit: { entity: MOCK_1DAY_SENIOR_PERMIT_ENTITY, expanded: {} }
372
+ }
373
+ }
374
+ ])
375
+
376
+ findLinkedRecurringPayment.mockResolvedValueOnce({
377
+ id: 'rcp-789',
378
+ status: 1
379
+ })
380
+
381
+ const result = await server.inject({ method: 'GET', url: baseUrl })
382
+ const body = JSON.parse(result.payload)
383
+
384
+ expect({
385
+ statusCode: result.statusCode,
386
+ body
387
+ }).toMatchObject({
388
+ statusCode: 200,
389
+ body: {
390
+ permission: expect.objectContaining({
391
+ ...MOCK_EXISTING_PERMISSION_ENTITY.toJSON(),
392
+ licensee: MOCK_EXISTING_CONTACT_ENTITY.toJSON(),
393
+ concessions: [],
394
+ permit: MOCK_1DAY_SENIOR_PERMIT_ENTITY.toJSON()
395
+ }),
396
+ recurringPayment: expect.objectContaining({ id: 'rcp-789', status: 1 })
397
+ }
398
+ })
399
+ })
400
+ })
401
+ })
402
+
403
+ it('changes reference number to uppercase', async () => {
404
+ const sampleQueryReferenceNumber = 'abc123'
405
+ const sampleResultReferenceNumber = sampleQueryReferenceNumber.toUpperCase()
406
+ const makeMockEntity = (obj = {}) => ({
407
+ ...obj,
408
+ toJSON: () => obj
409
+ })
410
+ executeQuery.mockReturnValueOnce([{ entity: { id: 'hgk-999' } }]).mockReturnValueOnce([
411
+ {
412
+ entity: makeMockEntity({
413
+ referenceNumber: sampleResultReferenceNumber
414
+ }),
415
+ expanded: {
416
+ concessionProofs: [],
417
+ licensee: { entity: makeMockEntity() },
418
+ permit: { entity: makeMockEntity() }
419
+ }
420
+ }
421
+ ])
422
+ const mockRequest = {
423
+ query: { licenseeBirthDate: '', licenseePostcode: '' },
424
+ params: { referenceNumber: sampleQueryReferenceNumber }
425
+ }
426
+ const mockResponseToolkit = { response: jest.fn(() => ({ code: () => {} })) }
427
+
428
+ await handler(mockRequest, mockResponseToolkit)
429
+
430
+ expect(mockResponseToolkit.response).toHaveBeenCalledWith(
431
+ expect.objectContaining({
432
+ permission: expect.objectContaining({
433
+ referenceNumber: sampleResultReferenceNumber
434
+ })
435
+ })
436
+ )
437
+ })
193
438
  })
@@ -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 { licenseeBirthDate, licenseePostcode } = request.query
28
- const contacts = await executeWithErrorLog(contactForLicenseeNoReference(licenseeBirthDate, licenseePostcode))
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
- method: TRANSACTION_SOURCE.govPay,
724
- source: PAYMENT_TYPE.debit,
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 rcp = await findNewestExistingRecurringPaymentInCrm()
780
- expect(RecurringPayment.fromResponse.mock.results[0].value).toBe(rcp)
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 rcp = await findNewestExistingRecurringPaymentInCrm()
815
- expect(rcp.id).toBe(expectedId)
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 rcp = await findNewestExistingRecurringPaymentInCrm()
821
- expect(rcp).toBeFalsy()
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: TRANSACTION_SOURCE.govPay,
140
- source: PAYMENT_TYPE.debit,
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
+ }