@defra-fish/sales-api-service 1.76.0-rc.2 → 1.76.0-rc.3

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.2",
3
+ "version": "1.76.0-rc.3",
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.2",
39
- "@defra-fish/connectors-lib": "1.76.0-rc.2",
40
- "@defra-fish/dynamics-lib": "1.76.0-rc.2",
38
+ "@defra-fish/business-rules-lib": "1.76.0-rc.3",
39
+ "@defra-fish/connectors-lib": "1.76.0-rc.3",
40
+ "@defra-fish/dynamics-lib": "1.76.0-rc.3",
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": "254132463f912d406c9260b08e694e20dbc2cc46"
56
+ "gitHead": "45c9601f13483b2b497347a5bd129f97b945bd3c"
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 sqs.sendMessage({
64
- QueueUrl: TRANSACTION_QUEUE.Url,
65
- MessageGroupId: id,
66
- MessageDeduplicationId: id,
67
- MessageBody: JSON.stringify({ id })
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