@defra-fish/sales-api-service 1.63.0-rc.0 → 1.63.0-rc.10
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/__tests__/transaction.schema.spec.js +61 -12
- package/src/schema/transaction.schema.js +8 -1
- package/src/server/__tests__/server.spec.js +27 -1
- package/src/server/routes/__tests__/transactions.spec.js +48 -0
- package/src/server/routes/transactions.js +26 -0
- package/src/server/server.js +13 -1
- package/src/services/__tests__/recurring-payments.service.spec.js +12 -1
- package/src/services/recurring-payments.service.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/sales-api-service",
|
|
3
|
-
"version": "1.63.0-rc.
|
|
3
|
+
"version": "1.63.0-rc.10",
|
|
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.63.0-rc.
|
|
39
|
-
"@defra-fish/connectors-lib": "1.63.0-rc.
|
|
40
|
-
"@defra-fish/dynamics-lib": "1.63.0-rc.
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.63.0-rc.10",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.63.0-rc.10",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.63.0-rc.10",
|
|
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": "c4301461b148cbf244c1bb5c834521243ec0a534"
|
|
56
56
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createTransactionSchema,
|
|
3
|
+
createTransactionResponseSchema,
|
|
4
|
+
finaliseTransactionResponseSchema,
|
|
5
|
+
retrieveStagedTransactionParamsSchema
|
|
6
|
+
} from '../transaction.schema.js'
|
|
2
7
|
import { mockTransactionPayload, mockStagedTransactionRecord, mockFinalisedTransactionRecord } from '../../__mocks__/test-data.js'
|
|
3
8
|
|
|
4
9
|
jest.mock('../validators/validators.js', () => ({
|
|
@@ -96,27 +101,42 @@ describe('createTransactionSchema', () => {
|
|
|
96
101
|
await expect(createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow()
|
|
97
102
|
})
|
|
98
103
|
|
|
99
|
-
it('validates successfully when
|
|
104
|
+
it('validates successfully when recurring payment detail is supplied', async () => {
|
|
100
105
|
const mockPayload = mockTransactionPayload()
|
|
101
|
-
mockPayload.
|
|
106
|
+
mockPayload.recurringPayment = {
|
|
107
|
+
agreementId: 't3jl08v2nqqmujrnhs09pmhtjx',
|
|
108
|
+
id: 'fdc73d20-a0bf-4da6-9a49-2f0a24bd3509'
|
|
109
|
+
}
|
|
102
110
|
await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow()
|
|
103
111
|
})
|
|
104
112
|
|
|
105
|
-
it('validates successfully when
|
|
113
|
+
it('validates successfully when recurring payment detail is omitted', async () => {
|
|
106
114
|
const mockPayload = mockTransactionPayload()
|
|
107
115
|
await expect(createTransactionSchema.validateAsync(mockPayload)).resolves.not.toThrow()
|
|
108
116
|
})
|
|
109
117
|
|
|
118
|
+
it('fails validation if agreement id is omitted from recurring payment detail', async () => {
|
|
119
|
+
const mockPayload = mockTransactionPayload()
|
|
120
|
+
mockPayload.recurringPayment = { id: 'fdc73d20-a0bf-4da6-9a49-2f0a24bd3509' }
|
|
121
|
+
await expect(() => createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow()
|
|
122
|
+
})
|
|
123
|
+
|
|
110
124
|
it.each([
|
|
111
|
-
['too
|
|
112
|
-
['too
|
|
113
|
-
['
|
|
114
|
-
['null', null],
|
|
115
|
-
['numeric', 4567]
|
|
116
|
-
|
|
125
|
+
['agreement id is too long', { agreementId: 'thisistoolongtobeanagreementid' }],
|
|
126
|
+
['agreement id is too short', { agreementId: 'tooshorttobeanagreementid' }],
|
|
127
|
+
['agreement id contains invalid characters', '!3j@08v2nqqmujrnhs09_mhtjx'],
|
|
128
|
+
['agreement id is null', { agreementId: null }],
|
|
129
|
+
['agreement id is a numeric', { agreementId: 4567 }],
|
|
130
|
+
['id is not a guid', { id: 'not-a-guid' }],
|
|
131
|
+
['id is null', { id: null }]
|
|
132
|
+
])('fails validation if %s', async (_d, recurringPayment) => {
|
|
117
133
|
const mockPayload = mockTransactionPayload()
|
|
118
|
-
mockPayload.
|
|
119
|
-
|
|
134
|
+
mockPayload.recurringPayment = {
|
|
135
|
+
agreementId: 'jhyu78iujhy7u87y6thu87uyj8',
|
|
136
|
+
id: '7a0660ec-8535-4357-b925-e598a9358119',
|
|
137
|
+
...recurringPayment
|
|
138
|
+
}
|
|
139
|
+
await expect(() => createTransactionSchema.validateAsync(mockPayload)).rejects.toThrow()
|
|
120
140
|
})
|
|
121
141
|
})
|
|
122
142
|
|
|
@@ -135,3 +155,32 @@ describe('finaliseTransactionResponseSchema', () => {
|
|
|
135
155
|
expect(result).toBeInstanceOf(Object)
|
|
136
156
|
})
|
|
137
157
|
})
|
|
158
|
+
|
|
159
|
+
describe('retrieveStagedTransactionParamsSchema', () => {
|
|
160
|
+
it.each([
|
|
161
|
+
['36fb757c-6377-49c5-ab6e-32eb9782fcf0'],
|
|
162
|
+
['c290b78d-3bbc-4445-b4dd-b36f6ee044a2'],
|
|
163
|
+
['2323a890-b36f-47b1-ab9f-d60e292ac4ae'],
|
|
164
|
+
['9c6b79be-28be-4916-aa5c-08520aa1e804']
|
|
165
|
+
])('validates successfully when a uuid v4 transactionId is %s', async transactionId => {
|
|
166
|
+
const sampleData = { id: transactionId }
|
|
167
|
+
await expect(retrieveStagedTransactionParamsSchema.validateAsync(sampleData)).resolves.not.toThrow()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it.each([
|
|
171
|
+
['uuid1 string', '5a429f62-871b-11ef-b864-0242ac120002'],
|
|
172
|
+
['uuid2 string', '000003e8-871b-21ef-8000-325096b39f47'],
|
|
173
|
+
['uuid3 string', 'a3bb189e-8bf9-3888-9912-ace4e6543002'],
|
|
174
|
+
['uuid5 string', 'a6edc906-2f9f-5fb2-a373-efac406f0ef2'],
|
|
175
|
+
['uuid6 string', 'a3bb189e-8bf9-3888-9912-ace4e6543002'],
|
|
176
|
+
['uuid7 string', '01927705-ffac-77b5-89af-c97451b1bbe2'],
|
|
177
|
+
['numeric', 4567]
|
|
178
|
+
])('fails validation when provided with a %s for transactionId', async (_d, transactionId) => {
|
|
179
|
+
const sampleData = { id: transactionId }
|
|
180
|
+
await expect(() => retrieveStagedTransactionParamsSchema.validateAsync(sampleData)).rejects.toThrow()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('throws an error if id missing', async () => {
|
|
184
|
+
await expect(() => retrieveStagedTransactionParamsSchema.validateAsync({}).rejects.toThrow())
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -36,7 +36,10 @@ const createTransactionRequestSchemaContent = {
|
|
|
36
36
|
createdBy: Joi.string().optional(),
|
|
37
37
|
journalId: Joi.string().optional(),
|
|
38
38
|
transactionId: Joi.string().guid({ version: 'uuidv4' }).optional(),
|
|
39
|
-
|
|
39
|
+
recurringPayment: Joi.object({
|
|
40
|
+
agreementId: Joi.string().alphanum().length(AGREEMENT_ID_LENGTH).required(),
|
|
41
|
+
id: Joi.string().guid()
|
|
42
|
+
}).optional()
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
/**
|
|
@@ -153,3 +156,7 @@ export const finaliseTransactionResponseSchema = Joi.object({
|
|
|
153
156
|
.required()
|
|
154
157
|
.label('finalise-transaction-status')
|
|
155
158
|
}).label('finalise-transaction-response')
|
|
159
|
+
|
|
160
|
+
export const retrieveStagedTransactionParamsSchema = Joi.object({
|
|
161
|
+
id: Joi.string().guid({ version: 'uuidv4' }).required()
|
|
162
|
+
})
|
|
@@ -2,9 +2,19 @@ import initialiseServer from '../server.js'
|
|
|
2
2
|
import Boom from '@hapi/boom'
|
|
3
3
|
import dotProp from 'dot-prop'
|
|
4
4
|
import { SERVER } from '../../config.js'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
|
|
7
|
+
jest.mock('fs', () => {
|
|
8
|
+
const actual = jest.requireActual('fs')
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
readFileSync: jest.fn(() => JSON.stringify({ name: 'sales-api-test', version: '1.2.3' }))
|
|
12
|
+
}
|
|
13
|
+
})
|
|
5
14
|
|
|
6
15
|
describe('hapi server', () => {
|
|
7
16
|
describe('initialisation', () => {
|
|
17
|
+
const serverInfoUri = 'test'
|
|
8
18
|
let serverConfigSpy
|
|
9
19
|
beforeAll(() => {
|
|
10
20
|
const Hapi = jest.requireActual('@hapi/hapi')
|
|
@@ -15,7 +25,7 @@ describe('hapi server', () => {
|
|
|
15
25
|
register: jest.fn(),
|
|
16
26
|
route: jest.fn(),
|
|
17
27
|
info: {
|
|
18
|
-
uri:
|
|
28
|
+
uri: serverInfoUri
|
|
19
29
|
},
|
|
20
30
|
listener: {}
|
|
21
31
|
}))
|
|
@@ -48,6 +58,22 @@ describe('hapi server', () => {
|
|
|
48
58
|
headersTimeout: 5123
|
|
49
59
|
})
|
|
50
60
|
})
|
|
61
|
+
|
|
62
|
+
it('logs startup details including name and version', async () => {
|
|
63
|
+
const mockPkg = { name: 'sales-api-test', version: '1.2.3' }
|
|
64
|
+
fs.readFileSync.mockReturnValue(JSON.stringify(mockPkg))
|
|
65
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
66
|
+
|
|
67
|
+
await initialiseServer({ port: 4000 })
|
|
68
|
+
|
|
69
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
70
|
+
expect.stringContaining('Server started at %s. Listening on %s. name: %s. version: %s'),
|
|
71
|
+
expect.any(String),
|
|
72
|
+
serverInfoUri,
|
|
73
|
+
mockPkg.name,
|
|
74
|
+
mockPkg.version
|
|
75
|
+
)
|
|
76
|
+
})
|
|
51
77
|
})
|
|
52
78
|
|
|
53
79
|
describe('configuration', () => {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import initialiseServer from '../../server.js'
|
|
2
2
|
import { mockTransactionPayload, mockStagedTransactionRecord } from '../../../__mocks__/test-data.js'
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid'
|
|
4
|
+
import { retrieveStagedTransactionParamsSchema } from '../../../schema/transaction.schema.js'
|
|
5
|
+
import { retrieveStagedTransaction } from '../../../services/transactions/retrieve-transaction.js'
|
|
6
|
+
import transactions from '../transactions.js'
|
|
7
|
+
|
|
4
8
|
jest.mock('../../../services/transactions/transactions.service.js', () => ({
|
|
5
9
|
createTransaction: jest.fn(async () => mockStagedTransactionRecord()),
|
|
6
10
|
createTransactions: jest.fn(async payloads => Array(payloads.length).fill(mockStagedTransactionRecord())),
|
|
@@ -9,6 +13,10 @@ jest.mock('../../../services/transactions/transactions.service.js', () => ({
|
|
|
9
13
|
processDlq: jest.fn(async () => {})
|
|
10
14
|
}))
|
|
11
15
|
|
|
16
|
+
jest.mock('../../../services/transactions/retrieve-transaction.js', () => ({
|
|
17
|
+
retrieveStagedTransaction: jest.fn()
|
|
18
|
+
}))
|
|
19
|
+
|
|
12
20
|
jest.mock('../../../schema/validators/validators.js', () => ({
|
|
13
21
|
...jest.requireActual('../../../schema/validators/validators.js'),
|
|
14
22
|
createOptionSetValidator: () => async () => undefined,
|
|
@@ -18,6 +26,20 @@ jest.mock('../../../schema/validators/validators.js', () => ({
|
|
|
18
26
|
createPermitConcessionValidator: () => async () => undefined
|
|
19
27
|
}))
|
|
20
28
|
|
|
29
|
+
jest.mock('@defra-fish/connectors-lib', () => {
|
|
30
|
+
const awsMock = {
|
|
31
|
+
docClient: {
|
|
32
|
+
get: jest.fn(() => ({ Item: { id: 'abc123' } }))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
AWS: jest.fn(() => awsMock),
|
|
37
|
+
airbrake: {
|
|
38
|
+
initialise: jest.fn()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
21
43
|
let server = null
|
|
22
44
|
|
|
23
45
|
describe('transaction handler', () => {
|
|
@@ -214,4 +236,30 @@ describe('transaction handler', () => {
|
|
|
214
236
|
expect(result).toBeUnprocessableEntityErrorResponse()
|
|
215
237
|
})
|
|
216
238
|
})
|
|
239
|
+
|
|
240
|
+
describe('retrieveStagedTransaction', () => {
|
|
241
|
+
const getMockRequest = ({ id = 'abc123' }) => ({ params: { id } })
|
|
242
|
+
const getMockResponseToolkit = () => ({ response: jest.fn() })
|
|
243
|
+
const retrieveHandler = transactions[transactions.length - 1].options.handler
|
|
244
|
+
|
|
245
|
+
it('handler should return continue response', async () => {
|
|
246
|
+
const request = getMockRequest({})
|
|
247
|
+
const responseToolkit = getMockResponseToolkit()
|
|
248
|
+
expect(await retrieveHandler(request, responseToolkit)).toEqual(responseToolkit.continue)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should call retrieveStagedTransaction with id', async () => {
|
|
252
|
+
const id = 'transaction-id'
|
|
253
|
+
const request = getMockRequest({ id })
|
|
254
|
+
await retrieveHandler(request, getMockResponseToolkit())
|
|
255
|
+
expect(retrieveStagedTransaction).toHaveBeenCalledWith(id)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should validate with cancelRecurringPaymentRequestParamsSchema', async () => {
|
|
259
|
+
const id = 'transaction-id'
|
|
260
|
+
const request = getMockRequest({ id })
|
|
261
|
+
await retrieveHandler(request, getMockResponseToolkit())
|
|
262
|
+
expect(transactions[5].options.validate.params).toBe(retrieveStagedTransactionParamsSchema)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
217
265
|
})
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
processQueue,
|
|
8
8
|
processDlq
|
|
9
9
|
} from '../../services/transactions/transactions.service.js'
|
|
10
|
+
import { retrieveStagedTransaction } from '../../services/transactions/retrieve-transaction.js'
|
|
10
11
|
import {
|
|
11
12
|
createTransactionSchema,
|
|
12
13
|
createTransactionResponseSchema,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
createTransactionBatchResponseSchema,
|
|
15
16
|
finaliseTransactionRequestSchema,
|
|
16
17
|
finaliseTransactionResponseSchema,
|
|
18
|
+
retrieveStagedTransactionParamsSchema,
|
|
17
19
|
BATCH_CREATE_MAX_COUNT
|
|
18
20
|
} from '../../schema/transaction.schema.js'
|
|
19
21
|
import db from 'debug'
|
|
@@ -174,5 +176,29 @@ export default [
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
method: 'GET',
|
|
182
|
+
path: '/retrieveStagedTransaction/{id}',
|
|
183
|
+
options: {
|
|
184
|
+
handler: async (request, h) => {
|
|
185
|
+
const { id } = request.params
|
|
186
|
+
const result = await retrieveStagedTransaction(id)
|
|
187
|
+
return h.response(result)
|
|
188
|
+
},
|
|
189
|
+
description: 'Retrieve a staged transaction',
|
|
190
|
+
tags: ['api', 'transactions'],
|
|
191
|
+
validate: {
|
|
192
|
+
params: retrieveStagedTransactionParamsSchema
|
|
193
|
+
},
|
|
194
|
+
plugins: {
|
|
195
|
+
'hapi-swagger': {
|
|
196
|
+
responses: {
|
|
197
|
+
200: { description: 'Staged transaction retreived' }
|
|
198
|
+
},
|
|
199
|
+
order: 5
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
177
203
|
}
|
|
178
204
|
]
|
package/src/server/server.js
CHANGED
|
@@ -8,6 +8,8 @@ import Boom from '@hapi/boom'
|
|
|
8
8
|
import { SERVER } from '../config.js'
|
|
9
9
|
import moment from 'moment'
|
|
10
10
|
import { airbrake } from '@defra-fish/connectors-lib'
|
|
11
|
+
import path from 'path'
|
|
12
|
+
import fs from 'fs'
|
|
11
13
|
|
|
12
14
|
export default async (opts = { port: SERVER.Port }) => {
|
|
13
15
|
airbrake.initialise()
|
|
@@ -49,7 +51,17 @@ export default async (opts = { port: SERVER.Port }) => {
|
|
|
49
51
|
server.route(Routes)
|
|
50
52
|
|
|
51
53
|
await server.start()
|
|
52
|
-
|
|
54
|
+
|
|
55
|
+
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
57
|
+
|
|
58
|
+
console.log(
|
|
59
|
+
'Server started at %s. Listening on %s. name: %s. version: %s',
|
|
60
|
+
moment().toISOString(),
|
|
61
|
+
server.info.uri,
|
|
62
|
+
pkg.name,
|
|
63
|
+
pkg.version
|
|
64
|
+
)
|
|
53
65
|
|
|
54
66
|
const shutdown = async code => {
|
|
55
67
|
await server.stop()
|
|
@@ -392,7 +392,9 @@ describe('recurring payments service', () => {
|
|
|
392
392
|
...permission
|
|
393
393
|
}
|
|
394
394
|
],
|
|
395
|
-
|
|
395
|
+
recurringPayment: {
|
|
396
|
+
agreementId
|
|
397
|
+
},
|
|
396
398
|
payment: {
|
|
397
399
|
amount: 35.8,
|
|
398
400
|
source: 'Gov Pay',
|
|
@@ -528,6 +530,15 @@ describe('recurring payments service', () => {
|
|
|
528
530
|
await expect(generateRecurringPaymentRecord(sampleTransaction)).rejects.toThrow('Invalid dates provided for permission')
|
|
529
531
|
})
|
|
530
532
|
|
|
533
|
+
it('returns a false flag when recurringPayment is not present', async () => {
|
|
534
|
+
const sampleTransaction = createFinalisedSampleTransaction()
|
|
535
|
+
delete sampleTransaction.recurringPayment
|
|
536
|
+
|
|
537
|
+
const rpRecord = await generateRecurringPaymentRecord(sampleTransaction)
|
|
538
|
+
|
|
539
|
+
expect(rpRecord.payment?.recurring).toBeFalsy()
|
|
540
|
+
})
|
|
541
|
+
|
|
531
542
|
it('returns a false flag when agreementId is not present', async () => {
|
|
532
543
|
const sampleTransaction = createFinalisedSampleTransaction(
|
|
533
544
|
null,
|
|
@@ -37,8 +37,8 @@ const getNextDueDate = (startDate, issueDate, endDate) => {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export const generateRecurringPaymentRecord = async (transactionRecord, permission) => {
|
|
40
|
-
if (transactionRecord.agreementId) {
|
|
41
|
-
const agreementResponse = await getRecurringPaymentAgreement(transactionRecord.agreementId)
|
|
40
|
+
if (transactionRecord.recurringPayment?.agreementId) {
|
|
41
|
+
const agreementResponse = await getRecurringPaymentAgreement(transactionRecord.recurringPayment.agreementId)
|
|
42
42
|
const lastDigitsCardNumbers = agreementResponse.payment_instrument?.card_details?.last_digits_card_number
|
|
43
43
|
const [{ startDate, issueDate, endDate }] = transactionRecord.permissions
|
|
44
44
|
return {
|
|
@@ -49,7 +49,7 @@ export const generateRecurringPaymentRecord = async (transactionRecord, permissi
|
|
|
49
49
|
cancelledDate: null,
|
|
50
50
|
cancelledReason: null,
|
|
51
51
|
endDate,
|
|
52
|
-
agreementId: transactionRecord.agreementId,
|
|
52
|
+
agreementId: transactionRecord.recurringPayment.agreementId,
|
|
53
53
|
status: 1,
|
|
54
54
|
last_digits_card_number: lastDigitsCardNumbers
|
|
55
55
|
}
|