@defra-fish/sales-api-service 1.76.0-rc.2 → 1.76.0-rc.4
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.76.0-rc.
|
|
3
|
+
"version": "1.76.0-rc.4",
|
|
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.76.0-rc.
|
|
39
|
-
"@defra-fish/connectors-lib": "1.76.0-rc.
|
|
40
|
-
"@defra-fish/dynamics-lib": "1.76.0-rc.
|
|
38
|
+
"@defra-fish/business-rules-lib": "1.76.0-rc.4",
|
|
39
|
+
"@defra-fish/connectors-lib": "1.76.0-rc.4",
|
|
40
|
+
"@defra-fish/dynamics-lib": "1.76.0-rc.4",
|
|
41
41
|
"@hapi/boom": "9.1.2",
|
|
42
42
|
"@hapi/hapi": "20.1.3",
|
|
43
43
|
"@hapi/inert": "6.0.3",
|
|
@@ -53,5 +53,5 @@
|
|
|
53
53
|
"moment-timezone": "0.5.34",
|
|
54
54
|
"uuid": "8.3.2"
|
|
55
55
|
},
|
|
56
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "92ce370aa6b17da8ad236ba2bc071240bc677ed1"
|
|
57
57
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`transaction service finaliseTransaction transaction rollback throws an internal server error if the SQS message fails to send 1`] = `"Failed to send transaction to staging queue: SQS send message failed"`;
|
|
4
|
+
|
|
5
|
+
exports[`transaction service finaliseTransaction transaction rollback throws an internal server error if the rollback fails 1`] = `"Failed to rollback transaction record b364c12f-ce62-4c62-b4bd-4a06fd57e256 after SQS send failure: Rollback failed"`;
|
|
@@ -12,9 +12,12 @@ import moment from 'moment'
|
|
|
12
12
|
import { TRANSACTION_STATUS } from '../constants.js'
|
|
13
13
|
import permissionsService from '../../permissions.service.js'
|
|
14
14
|
import { AWS } from '@defra-fish/connectors-lib'
|
|
15
|
+
import db from 'debug'
|
|
15
16
|
|
|
16
17
|
const { START_AFTER_PAYMENT_MINUTES } = BusinessRulesLib
|
|
17
18
|
const { docClient, sqs } = AWS.mock.results[0].value
|
|
19
|
+
const debugLogger = db.mock.results[1].value // first call is by retrieve-transaction.js
|
|
20
|
+
global.structuredClone = global.structuredClone ?? (obj => JSON.parse(JSON.stringify(obj)))
|
|
18
21
|
|
|
19
22
|
jest.mock('../../permissions.service.js', () => ({
|
|
20
23
|
generatePermissionNumber: jest.fn(() => MOCK_PERMISSION_NUMBER),
|
|
@@ -50,6 +53,8 @@ jest.mock('@defra-fish/connectors-lib', () => {
|
|
|
50
53
|
}
|
|
51
54
|
})
|
|
52
55
|
|
|
56
|
+
jest.mock('debug', () => jest.fn(() => jest.fn()))
|
|
57
|
+
|
|
53
58
|
const getStagedTransactionRecord = () => {
|
|
54
59
|
const record = mockStagedTransactionRecord()
|
|
55
60
|
const {
|
|
@@ -265,6 +270,107 @@ describe('transaction service', () => {
|
|
|
265
270
|
docClient.get.mockRejectedValueOnce(new Error('Test error'))
|
|
266
271
|
await expect(finaliseTransaction(mockTransactionPayload())).rejects.toThrow('Test error')
|
|
267
272
|
})
|
|
273
|
+
|
|
274
|
+
describe('transaction rollback', () => {
|
|
275
|
+
const sqsFailureSetup = ({
|
|
276
|
+
mockRecord = mockStagedTransactionRecord(),
|
|
277
|
+
updateExpression = { expression: 'update-expression' }
|
|
278
|
+
} = {}) => {
|
|
279
|
+
docClient.get.mockResolvedValueOnce({ Item: mockRecord })
|
|
280
|
+
docClient.createUpdateExpression.mockReturnValueOnce({}).mockReturnValueOnce(updateExpression)
|
|
281
|
+
const { id: _omitId, ...originalRecord } = JSON.parse(JSON.stringify(mockRecord))
|
|
282
|
+
sqs.sendMessage.mockRejectedValueOnce(new Error('SQS send message failed'))
|
|
283
|
+
return { mockRecord, originalRecord }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const getSamplePayment = () => ({
|
|
287
|
+
amount: 30,
|
|
288
|
+
timestamp: new Date().toISOString()
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('rolls back the transaction if the SQS message fails to send', async () => {
|
|
292
|
+
const updateExpression = { expression: Symbol('update-expression') }
|
|
293
|
+
const { mockRecord } = sqsFailureSetup({ updateExpression })
|
|
294
|
+
try {
|
|
295
|
+
await finaliseTransaction({
|
|
296
|
+
id: mockRecord.id,
|
|
297
|
+
payment: getSamplePayment()
|
|
298
|
+
})
|
|
299
|
+
} catch (e) {}
|
|
300
|
+
|
|
301
|
+
expect(docClient.update).toHaveBeenNthCalledWith(
|
|
302
|
+
2,
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
TableName: TRANSACTION_STAGING_TABLE.TableName,
|
|
305
|
+
Key: { id: mockRecord.id },
|
|
306
|
+
...updateExpression
|
|
307
|
+
})
|
|
308
|
+
)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('uses createUpdateExpression to roll back the transaction when the SQS message fails to send', async () => {
|
|
312
|
+
const updateExpression = { expression: Symbol('update-expression') }
|
|
313
|
+
const { mockRecord, originalRecord } = sqsFailureSetup({
|
|
314
|
+
mockRecord: {
|
|
315
|
+
...mockStagedTransactionRecord(),
|
|
316
|
+
uniqueTag: 'afd113fe-acfb-4124-a2ed-f1b182df55fd'
|
|
317
|
+
},
|
|
318
|
+
updateExpression
|
|
319
|
+
})
|
|
320
|
+
try {
|
|
321
|
+
await finaliseTransaction({
|
|
322
|
+
id: mockRecord.id,
|
|
323
|
+
payment: getSamplePayment()
|
|
324
|
+
})
|
|
325
|
+
} catch (e) {}
|
|
326
|
+
|
|
327
|
+
expect(docClient.createUpdateExpression).toHaveBeenNthCalledWith(2, originalRecord)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('throws an internal server error if the SQS message fails to send', async () => {
|
|
331
|
+
const {
|
|
332
|
+
mockRecord: { id }
|
|
333
|
+
} = sqsFailureSetup()
|
|
334
|
+
|
|
335
|
+
await expect(
|
|
336
|
+
finaliseTransaction({
|
|
337
|
+
id,
|
|
338
|
+
payment: getSamplePayment()
|
|
339
|
+
})
|
|
340
|
+
).rejects.toThrowErrorMatchingSnapshot()
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('logs the error if the SQS message fails to send', async () => {
|
|
344
|
+
const {
|
|
345
|
+
mockRecord: { id }
|
|
346
|
+
} = sqsFailureSetup()
|
|
347
|
+
try {
|
|
348
|
+
await finaliseTransaction({ id, payment: getSamplePayment() })
|
|
349
|
+
} catch (e) {}
|
|
350
|
+
expect(debugLogger).toHaveBeenCalledWith(
|
|
351
|
+
'Error sending transaction %s to staging queue: %o',
|
|
352
|
+
id,
|
|
353
|
+
expect.objectContaining({
|
|
354
|
+
message: 'Failed to send transaction to staging queue: SQS send message failed',
|
|
355
|
+
isBoom: true
|
|
356
|
+
})
|
|
357
|
+
)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('throws an internal server error if the rollback fails', async () => {
|
|
361
|
+
const {
|
|
362
|
+
mockRecord: { id }
|
|
363
|
+
} = sqsFailureSetup()
|
|
364
|
+
docClient.update.mockResolvedValueOnce({ Attributes: { status: {} } })
|
|
365
|
+
docClient.update.mockRejectedValueOnce(new Error('Rollback failed'))
|
|
366
|
+
await expect(
|
|
367
|
+
finaliseTransaction({
|
|
368
|
+
id,
|
|
369
|
+
payment: getSamplePayment()
|
|
370
|
+
})
|
|
371
|
+
).rejects.toThrowErrorMatchingSnapshot()
|
|
372
|
+
})
|
|
373
|
+
})
|
|
268
374
|
})
|
|
269
375
|
|
|
270
376
|
describe('finaliseTransaction adjusts licence times according to issue date and start date', () => {
|
|
@@ -23,6 +23,7 @@ const getAdjustedStartDate = ({ issueDate, startDate, dataSource }) => {
|
|
|
23
23
|
export async function finaliseTransaction ({ id, ...payload }) {
|
|
24
24
|
debug('Finalising transaction %s', id)
|
|
25
25
|
const transactionRecord = await retrieveStagedTransaction(id)
|
|
26
|
+
const { id: _omitId, ...originalTransactionRecord } = structuredClone(transactionRecord)
|
|
26
27
|
|
|
27
28
|
if (transactionRecord.status?.id === TRANSACTION_STATUS.FINALISED) {
|
|
28
29
|
throw Boom.resourceGone('The transaction has already been finalised', transactionRecord)
|
|
@@ -60,12 +61,29 @@ export async function finaliseTransaction ({ id, ...payload }) {
|
|
|
60
61
|
})
|
|
61
62
|
debug('Updated transaction record for identifier %s', id)
|
|
62
63
|
|
|
63
|
-
const receipt = await
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const receipt = await (async () => {
|
|
65
|
+
try {
|
|
66
|
+
return await sqs.sendMessage({
|
|
67
|
+
QueueUrl: TRANSACTION_QUEUE.Url,
|
|
68
|
+
MessageGroupId: id,
|
|
69
|
+
MessageDeduplicationId: id,
|
|
70
|
+
MessageBody: JSON.stringify({ id })
|
|
71
|
+
})
|
|
72
|
+
} catch (error) {
|
|
73
|
+
try {
|
|
74
|
+
await docClient.update({
|
|
75
|
+
TableName: TRANSACTION_STAGING_TABLE.TableName,
|
|
76
|
+
Key: { id },
|
|
77
|
+
...docClient.createUpdateExpression(originalTransactionRecord),
|
|
78
|
+
ReturnValues: 'ALL_NEW'
|
|
79
|
+
})
|
|
80
|
+
} catch (rollbackError) {
|
|
81
|
+
throw Boom.internal(`Failed to rollback transaction record ${id} after SQS send failure`, rollbackError)
|
|
82
|
+
}
|
|
83
|
+
debug('Error sending transaction %s to staging queue: %o', id, error)
|
|
84
|
+
throw Boom.internal('Failed to send transaction to staging queue', error)
|
|
85
|
+
}
|
|
86
|
+
})()
|
|
69
87
|
|
|
70
88
|
debug('Sent transaction %s to staging queue with message-id %s', id, receipt.MessageId)
|
|
71
89
|
updatedRecord.status.messageId = receipt.MessageId
|