@defra-fish/sales-api-service 1.58.0 → 1.59.0-rc.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.
@@ -0,0 +1,99 @@
1
+ import { dueRecurringPaymentsResponseSchema } from '../recurring-payments.schema.js'
2
+
3
+ jest.mock('../validators/validators.js', () => ({
4
+ ...jest.requireActual('../validators/validators.js'),
5
+ createEntityIdValidator: () => () => {} // sample data so we don't want it validated for being a real entity id
6
+ }))
7
+
8
+ const getSampleData = () => ({
9
+ id: 'd5549fc6-41c1-ef11-b8e8-7c1e52215dc9',
10
+ name: 'test',
11
+ status: 0,
12
+ nextDueDate: '2025-01-22T00:00:00.000Z',
13
+ cancelledDate: null,
14
+ cancelledReason: null,
15
+ endDate: '2025-12-22T23:59:59.000Z',
16
+ agreementId: 'c756d22b-0003-4e24-a922-009f358852bd',
17
+ activePermission: 'cb549fc6-41c1-ef11-b8e8-7c1e52215dc9',
18
+ contactId: 'bf549fc6-41c1-ef11-b8e8-7c1e52215dc9',
19
+ publicId: 'q/kJYJrYNt/PkVbWChugMRSSxoDEttw1ownaNzDDyEw=',
20
+ expanded: {
21
+ contact: {
22
+ entity: {
23
+ id: 'bf549fc6-41c1-ef11-b8e8-7c1e52215dc9',
24
+ firstName: 'Recurring',
25
+ lastName: 'Test',
26
+ birthDate: '1991-01-01',
27
+ email: 'recurring.test@hotmail.com',
28
+ mobilePhone: null,
29
+ organisation: null,
30
+ premises: '1',
31
+ street: 'Catharine Place',
32
+ locality: null,
33
+ town: 'Bath',
34
+ postcode: 'BA1 2PR'
35
+ }
36
+ },
37
+ activePermission: {
38
+ entity: {
39
+ id: 'cb549fc6-41c1-ef11-b8e8-7c1e52215dc9',
40
+ referenceNumber: '23221225-2WC3FRT-ADQFJ6',
41
+ issueDate: '2024-12-23T15:22:49.000Z',
42
+ startDate: '2024-12-23T15:52:49.000Z',
43
+ endDate: '2025-12-22T23:59:59.000Z',
44
+ stagingId: 'a544cc77-156c-40e7-9c69-02e70829341d',
45
+ dataSource: {
46
+ id: 910400003,
47
+ label: 'Web Sales',
48
+ description: 'Web Sales'
49
+ }
50
+ }
51
+ }
52
+ }
53
+ })
54
+
55
+ describe('getDueRecurringPaymentsSchema', () => {
56
+ it('validates expected object', async () => {
57
+ await expect(() => dueRecurringPaymentsResponseSchema.validateAsync(getSampleData())).not.toThrow()
58
+ })
59
+
60
+ it.each([
61
+ 'id',
62
+ 'name',
63
+ 'status',
64
+ 'nextDueDate',
65
+ 'cancelledDate',
66
+ 'cancelledReason',
67
+ 'endDate',
68
+ 'agreementId',
69
+ 'activePermission',
70
+ 'contactId',
71
+ 'publicId'
72
+ ])('throws an error if %s is missing', async property => {
73
+ const sampleData = getSampleData()
74
+ delete sampleData[property]
75
+ expect(() => dueRecurringPaymentsResponseSchema.validateAsync(sampleData)).rejects.toThrow()
76
+ })
77
+
78
+ it.each([
79
+ ['id', 'not-a-guid'],
80
+ ['name', 99],
81
+ ['status', 'not-a-number'],
82
+ ['nextDueDate', 'not-a-date'],
83
+ ['cancelledDate', 'not-a-date'],
84
+ ['cancelledReason', 99],
85
+ ['endDate', 'not-a-date'],
86
+ ['agreementId', 'not-a-guid'],
87
+ ['activePermission', 'not-a-guid'],
88
+ ['contactId', 'still-not-a-guid'],
89
+ ['publicId', 99]
90
+ ])('throws an error if %s is not the correct type', async (property, value) => {
91
+ const sampleData = getSampleData()
92
+ sampleData[property] = value
93
+ expect(() => dueRecurringPaymentsResponseSchema.validateAsync(sampleData)).rejects.toThrow()
94
+ })
95
+
96
+ it('snapshot test schema', async () => {
97
+ expect(dueRecurringPaymentsResponseSchema).toMatchSnapshot()
98
+ })
99
+ })
@@ -5,11 +5,10 @@ import { permitSchema } from './permit.schema.js'
5
5
  import { contactResponseSchema } from './contact.schema.js'
6
6
  import { finalisedPermissionSchemaContent } from './permission.schema.js'
7
7
 
8
+ const REFERENCE_LENGTH = 6
9
+
8
10
  export const authenticateRenewalRequestParamsSchema = Joi.object({
9
- referenceNumber: Joi.string()
10
- .min(6)
11
- .required()
12
- .description('The permission reference number (supports partial)')
11
+ referenceNumber: Joi.string().min(REFERENCE_LENGTH).required().description('The permission reference number (supports partial)')
13
12
  }).label('authenticate-renewal-request-params')
14
13
 
15
14
  export const authenticateRenewalRequestQuerySchema = Joi.object({
@@ -5,25 +5,25 @@ import { optionSetOption } from './option-set.schema.js'
5
5
  import { createReferenceDataEntityValidator } from './validators/validators.js'
6
6
  import { Permit } from '@defra-fish/dynamics-lib'
7
7
  import { validation } from '@defra-fish/business-rules-lib'
8
- import { v4 as uuid } from 'uuid'
9
8
 
9
+ const DATE_EXAMPLE = '2025-01-01T00:00:00.000Z'
10
10
  const issueDateSchema = Joi.string()
11
11
  .isoDate()
12
12
  .required()
13
13
  .allow(null)
14
14
  .description('An ISO8601 compatible date string defining when the permission was issued')
15
- .example(new Date().toISOString())
15
+ .example(DATE_EXAMPLE)
16
16
  const startDateSchema = Joi.string()
17
17
  .isoDate()
18
18
  .required()
19
19
  .allow(null)
20
20
  .description('An ISO8601 compatible date string defining when the permission commences')
21
- .example(new Date().toISOString())
21
+ .example(DATE_EXAMPLE)
22
22
  const endDateSchema = Joi.string()
23
23
  .isoDate()
24
24
  .required()
25
25
  .description('An ISO8601 compatible date string defining when the permission expires')
26
- .example(new Date().toISOString())
26
+ .example(DATE_EXAMPLE)
27
27
 
28
28
  export const stagedPermissionSchema = Joi.object({
29
29
  permitId: Joi.string()
@@ -41,12 +41,12 @@ export const stagedPermissionSchema = Joi.object({
41
41
  }).label('staged-permission')
42
42
 
43
43
  export const finalisedPermissionSchemaContent = {
44
- id: Joi.string().guid().required().example(uuid()),
44
+ id: Joi.string().guid().required().example('a17fc331-141b-4fc0-8549-329d6934fadb'),
45
45
  referenceNumber: validation.permission.createPermissionNumberValidator(Joi),
46
46
  issueDate: issueDateSchema,
47
47
  startDate: startDateSchema,
48
48
  endDate: endDateSchema,
49
- stagingId: Joi.string().guid().required().example(uuid()),
49
+ stagingId: Joi.string().guid().required().example('a17fc331-141b-4fc0-8549-329d6934fadb'),
50
50
  dataSource: optionSetOption
51
51
  }
52
52
 
@@ -0,0 +1,21 @@
1
+ import Joi from 'joi'
2
+ import { commonContactSchema } from './contact.schema.js'
3
+ import { finalisedPermissionSchemaContent } from './permission.schema.js'
4
+
5
+ export const dueRecurringPaymentsResponseSchema = Joi.object({
6
+ id: Joi.string().guid().required(),
7
+ name: Joi.string().required(),
8
+ status: Joi.number().required(),
9
+ nextDueDate: Joi.string().isoDate().required(),
10
+ cancelledDate: Joi.string().isoDate().allow(null).required(),
11
+ cancelledReason: Joi.string().allow(null).required(),
12
+ endDate: Joi.string().isoDate().required(),
13
+ agreementId: Joi.string().guid().required(),
14
+ activePermission: Joi.string().guid().required(),
15
+ contactId: Joi.string().guid().required(),
16
+ publicId: Joi.string().required(),
17
+ expanded: Joi.object({
18
+ contact: { entity: commonContactSchema },
19
+ activePermission: { entity: finalisedPermissionSchemaContent }
20
+ })
21
+ })
@@ -1,4 +1,4 @@
1
- import { permissionForLicensee, executeQuery } from '@defra-fish/dynamics-lib'
1
+ import { contactForLicenseeNoReference, executeQuery } from '@defra-fish/dynamics-lib'
2
2
  import db from 'debug'
3
3
  jest.mock('@defra-fish/dynamics-lib')
4
4
  jest.mock('debug')
@@ -10,7 +10,7 @@ describe('executeWithErrorLog', () => {
10
10
  executeQuery.mockImplementation(() => {
11
11
  throw new Error()
12
12
  })
13
- permissionForLicensee.mockReturnValueOnce({ filter: 'query filter test' })
13
+ contactForLicenseeNoReference.mockReturnValueOnce({ filter: 'query filter test' })
14
14
  const authenticate = require('../authenticate.js').default
15
15
  const [
16
16
  {
@@ -1,5 +1,5 @@
1
1
  import initialiseServer from '../../server.js'
2
- import { executeQuery, permissionForLicensee } from '@defra-fish/dynamics-lib'
2
+ import { contactForLicenseeNoReference, executeQuery, permissionForContacts } from '@defra-fish/dynamics-lib'
3
3
  import {
4
4
  MOCK_EXISTING_PERMISSION_ENTITY,
5
5
  MOCK_EXISTING_CONTACT_ENTITY,
@@ -10,8 +10,9 @@ import {
10
10
 
11
11
  jest.mock('@defra-fish/dynamics-lib', () => ({
12
12
  ...jest.requireActual('@defra-fish/dynamics-lib'),
13
- permissionForLicensee: jest.fn(),
14
- executeQuery: jest.fn()
13
+ contactForLicenseeNoReference: jest.fn(),
14
+ executeQuery: jest.fn(),
15
+ permissionForContacts: jest.fn()
15
16
  }))
16
17
 
17
18
  let server = null
@@ -27,6 +28,12 @@ describe('authenticate handler', () => {
27
28
 
28
29
  describe('authenticateRenewal', () => {
29
30
  it('authenticates a renewal request', async () => {
31
+ executeQuery.mockResolvedValueOnce([
32
+ {
33
+ entity: MOCK_EXISTING_CONTACT_ENTITY,
34
+ expanded: {}
35
+ }
36
+ ])
30
37
  executeQuery.mockResolvedValueOnce([
31
38
  {
32
39
  entity: MOCK_EXISTING_PERMISSION_ENTITY,
@@ -42,7 +49,6 @@ describe('authenticate handler', () => {
42
49
  method: 'GET',
43
50
  url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
44
51
  })
45
- expect(permissionForLicensee).toHaveBeenCalledWith('CD379B', '2000-01-01', 'AB12 3CD')
46
52
  expect(result.statusCode).toBe(200)
47
53
  expect(JSON.parse(result.payload)).toMatchObject({
48
54
  permission: expect.objectContaining({
@@ -61,6 +67,12 @@ describe('authenticate handler', () => {
61
67
 
62
68
  describe('if no concessions are returned', () => {
63
69
  beforeEach(() => {
70
+ executeQuery.mockResolvedValueOnce([
71
+ {
72
+ entity: MOCK_EXISTING_CONTACT_ENTITY,
73
+ expanded: {}
74
+ }
75
+ ])
64
76
  executeQuery.mockResolvedValueOnce([
65
77
  {
66
78
  entity: MOCK_EXISTING_PERMISSION_ENTITY,
@@ -73,12 +85,20 @@ describe('authenticate handler', () => {
73
85
  ])
74
86
  })
75
87
 
76
- it('should call permissionForLicensee with the licence number, dob and postcode for a renewal request, ', async () => {
88
+ it('should call contactForLicenseeNoReference with dob and postcode for a renewal request', async () => {
89
+ await server.inject({
90
+ method: 'GET',
91
+ url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
92
+ })
93
+ expect(contactForLicenseeNoReference).toHaveBeenCalledWith('2000-01-01', 'AB12 3CD')
94
+ })
95
+
96
+ it('should call permissionForContacts with contact ids from contactForLicenseeNoReference', async () => {
77
97
  await server.inject({
78
98
  method: 'GET',
79
99
  url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
80
100
  })
81
- expect(permissionForLicensee).toHaveBeenCalledWith('CD379B', '2000-01-01', 'AB12 3CD')
101
+ expect(permissionForContacts).toHaveBeenCalledWith([MOCK_EXISTING_CONTACT_ENTITY.id])
82
102
  })
83
103
 
84
104
  it('returns 200 from a renewal request', async () => {
@@ -105,7 +125,13 @@ describe('authenticate handler', () => {
105
125
  })
106
126
 
107
127
  it('throws 500 errors if more than one result was found for the query', async () => {
108
- executeQuery.mockResolvedValueOnce([{}, {}])
128
+ executeQuery.mockResolvedValueOnce([
129
+ {
130
+ entity: MOCK_EXISTING_CONTACT_ENTITY,
131
+ expanded: {}
132
+ }
133
+ ])
134
+ executeQuery.mockResolvedValueOnce([{ entity: { referenceNumber: 'CD379B' } }, { entity: { referenceNumber: 'CD379B' } }])
109
135
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
110
136
  const result = await server.inject({
111
137
  method: 'GET',
@@ -121,6 +147,26 @@ describe('authenticate handler', () => {
121
147
  })
122
148
 
123
149
  it('throws 401 errors if the renewal could not be authenticated', async () => {
150
+ executeQuery.mockResolvedValueOnce([
151
+ {
152
+ entity: MOCK_EXISTING_CONTACT_ENTITY,
153
+ expanded: {}
154
+ }
155
+ ])
156
+ executeQuery.mockResolvedValueOnce([])
157
+ const result = await server.inject({
158
+ method: 'GET',
159
+ url: '/authenticate/renewal/CD379B?licenseeBirthDate=2000-01-01&licenseePostcode=AB12 3CD'
160
+ })
161
+ expect(result.statusCode).toBe(401)
162
+ expect(JSON.parse(result.payload)).toMatchObject({
163
+ error: 'Unauthorized',
164
+ message: 'The licensee could not be authenticated',
165
+ statusCode: 401
166
+ })
167
+ })
168
+
169
+ it('throws 401 errors if no contact to be authenticated', async () => {
124
170
  executeQuery.mockResolvedValueOnce([])
125
171
  const result = await server.inject({
126
172
  method: 'GET',
@@ -1,6 +1,12 @@
1
1
  import dueRecurringPayments from '../recurring-payments.js'
2
2
  import { getRecurringPayments } from '../../../services/recurring-payments.service.js'
3
3
 
4
+ const [
5
+ {
6
+ options: { handler: drpHandler }
7
+ }
8
+ ] = dueRecurringPayments
9
+
4
10
  jest.mock('../../../services/recurring-payments.service.js', () => ({
5
11
  getRecurringPayments: jest.fn()
6
12
  }))
@@ -19,13 +25,13 @@ describe('recurring payments', () => {
19
25
  it('handler should return continue response', async () => {
20
26
  const request = getMockRequest({})
21
27
  const responseToolkit = getMockResponseToolkit()
22
- expect(await dueRecurringPayments[0].handler(request, responseToolkit)).toEqual(responseToolkit.continue)
28
+ expect(await drpHandler(request, responseToolkit)).toEqual(responseToolkit.continue)
23
29
  })
24
30
 
25
31
  it('should call getRecurringPayments with date', async () => {
26
32
  const date = Symbol('date')
27
33
  const request = getMockRequest({ date })
28
- await dueRecurringPayments[0].handler(request, getMockResponseToolkit())
34
+ await drpHandler(request, getMockResponseToolkit())
29
35
  expect(getRecurringPayments).toHaveBeenCalledWith(date)
30
36
  })
31
37
  })
@@ -5,8 +5,9 @@ import {
5
5
  authenticateRenewalResponseSchema
6
6
  } from '../../schema/authenticate.schema.js'
7
7
  import db from 'debug'
8
- import { permissionForLicensee, concessionsByIds, executeQuery } from '@defra-fish/dynamics-lib'
8
+ import { permissionForContacts, concessionsByIds, executeQuery, contactForLicenseeNoReference } from '@defra-fish/dynamics-lib'
9
9
  const debug = db('sales:renewal-authentication')
10
+ const failAuthenticate = 'The licensee could not be authenticated'
10
11
 
11
12
  const executeWithErrorLog = async query => {
12
13
  try {
@@ -24,33 +25,37 @@ export default [
24
25
  options: {
25
26
  handler: async (request, h) => {
26
27
  const { licenseeBirthDate, licenseePostcode } = request.query
27
- const results = await executeWithErrorLog(
28
- permissionForLicensee(request.params.referenceNumber, licenseeBirthDate, licenseePostcode)
29
- )
30
-
31
- if (results.length === 1) {
32
- let concessionProofs = []
33
- if (results[0].expanded.concessionProofs.length > 0) {
34
- const ids = results[0].expanded.concessionProofs.map(f => f.entity.id)
35
- concessionProofs = await executeWithErrorLog(concessionsByIds(ids))
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')
36
56
  }
37
- return h
38
- .response({
39
- permission: {
40
- ...results[0].entity.toJSON(),
41
- licensee: results[0].expanded.licensee.entity.toJSON(),
42
- concessions: concessionProofs.map(c => ({
43
- id: c.expanded.concession.entity.id,
44
- proof: c.entity.toJSON()
45
- })),
46
- permit: results[0].expanded.permit.entity.toJSON()
47
- }
48
- })
49
- .code(200)
50
- } else if (results.length === 0) {
51
- throw Boom.unauthorized('The licensee could not be authenticated')
52
57
  } else {
53
- throw new Error('Unable to authenticate, non-unique results for query')
58
+ throw Boom.unauthorized(failAuthenticate)
54
59
  }
55
60
  },
56
61
  description: 'Authenticate a licensee by checking the licence number corresponds with the provided contact details',
@@ -66,7 +71,7 @@ export default [
66
71
  'hapi-swagger': {
67
72
  responses: {
68
73
  200: { description: 'The licensee was successfully authenticated', schema: authenticateRenewalResponseSchema },
69
- 401: { description: 'The licensee could not be authenticated' }
74
+ 401: { description: failAuthenticate }
70
75
  },
71
76
  order: 1
72
77
  }
@@ -1,12 +1,25 @@
1
+ import { dueRecurringPaymentsResponseSchema } from '../../schema/recurring-payments.schema.js'
1
2
  import { getRecurringPayments } from '../../services/recurring-payments.service.js'
2
3
  export default [
3
4
  {
4
5
  method: 'GET',
5
6
  path: '/dueRecurringPayments/{date}',
6
- handler: async (request, h) => {
7
- const { date } = request.params
8
- const result = await getRecurringPayments(date)
9
- return h.response(result)
7
+ options: {
8
+ handler: async (request, h) => {
9
+ const { date } = request.params
10
+ const result = await getRecurringPayments(date)
11
+ return h.response(result)
12
+ },
13
+ description: 'Retrieve recurring payments due for the specified date',
14
+ tags: ['api', 'recurring-payments'],
15
+ plugins: {
16
+ 'hapi-swagger': {
17
+ responses: {
18
+ 200: { description: 'Recurring payments due', schema: dueRecurringPaymentsResponseSchema }
19
+ },
20
+ order: 1
21
+ }
22
+ }
10
23
  }
11
24
  }
12
25
  ]