@defra-fish/sales-api-service 1.62.0-rc.0 → 1.62.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/sales-api-service",
3
- "version": "1.62.0-rc.0",
3
+ "version": "1.62.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.62.0-rc.0",
39
- "@defra-fish/connectors-lib": "1.62.0-rc.0",
40
- "@defra-fish/dynamics-lib": "1.62.0-rc.0",
38
+ "@defra-fish/business-rules-lib": "1.62.0-rc.10",
39
+ "@defra-fish/connectors-lib": "1.62.0-rc.10",
40
+ "@defra-fish/dynamics-lib": "1.62.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": "d56e1a0d0f4b22f1dded800c0ed511b737cb91a7"
55
+ "gitHead": "7be5ffd5ab38a7b7690cb2b7f7c711c5d8473860"
56
56
  }
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  dueRecurringPaymentsRequestParamsSchema,
3
3
  dueRecurringPaymentsResponseSchema,
4
- processRPResultRequestParamsSchema
4
+ processRPResultRequestParamsSchema,
5
+ cancelRecurringPaymentRequestParamsSchema
5
6
  } from '../recurring-payments.schema.js'
6
7
 
7
8
  jest.mock('../validators/validators.js', () => ({
@@ -149,3 +150,19 @@ describe('processRPResultRequestParamsSchema', () => {
149
150
  expect(() => processRPResultRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
150
151
  })
151
152
  })
153
+
154
+ describe('cancelRecurringPaymentRequestParamsSchema', () => {
155
+ it('validates expected object', async () => {
156
+ const sampleData = { id: 'abc123' }
157
+ expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData)).not.toThrow()
158
+ })
159
+
160
+ it('throws an error if id missing', async () => {
161
+ expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync({}).rejects.toThrow())
162
+ })
163
+
164
+ it('throws an error if id is not the correct type', async () => {
165
+ const sampleData = { id: 99 }
166
+ expect(() => cancelRecurringPaymentRequestParamsSchema.validateAsync(sampleData).rejects.toThrow())
167
+ })
168
+ })
@@ -29,3 +29,7 @@ export const processRPResultRequestParamsSchema = Joi.object({
29
29
  paymentId: Joi.string().required(),
30
30
  createdDate: Joi.string().isoDate().required()
31
31
  })
32
+
33
+ export const cancelRecurringPaymentRequestParamsSchema = Joi.object({
34
+ id: Joi.string().required()
35
+ })
@@ -1,6 +1,10 @@
1
1
  import recurringPayments from '../recurring-payments.js'
2
- import { getRecurringPayments, processRPResult } from '../../../services/recurring-payments.service.js'
3
- import { dueRecurringPaymentsRequestParamsSchema, processRPResultRequestParamsSchema } from '../../../schema/recurring-payments.schema.js'
2
+ import { getRecurringPayments, processRPResult, cancelRecurringPayment } from '../../../services/recurring-payments.service.js'
3
+ import {
4
+ dueRecurringPaymentsRequestParamsSchema,
5
+ processRPResultRequestParamsSchema,
6
+ cancelRecurringPaymentRequestParamsSchema
7
+ } from '../../../schema/recurring-payments.schema.js'
4
8
 
5
9
  const [
6
10
  {
@@ -8,17 +12,22 @@ const [
8
12
  },
9
13
  {
10
14
  options: { handler: prpHandler }
15
+ },
16
+ {
17
+ options: { handler: crpHandler }
11
18
  }
12
19
  ] = recurringPayments
13
20
 
14
21
  jest.mock('../../../services/recurring-payments.service.js', () => ({
15
22
  getRecurringPayments: jest.fn(),
16
- processRPResult: jest.fn()
23
+ processRPResult: jest.fn(),
24
+ cancelRecurringPayment: jest.fn()
17
25
  }))
18
26
 
19
27
  jest.mock('../../../schema/recurring-payments.schema.js', () => ({
20
28
  dueRecurringPaymentsRequestParamsSchema: jest.fn(),
21
- processRPResultRequestParamsSchema: jest.fn()
29
+ processRPResultRequestParamsSchema: jest.fn(),
30
+ cancelRecurringPaymentRequestParamsSchema: jest.fn()
22
31
  }))
23
32
 
24
33
  const getMockRequest = ({
@@ -27,9 +36,10 @@ const getMockRequest = ({
27
36
  paymentId = 'payment-id',
28
37
  createdDate = 'created-date',
29
38
  existingRecurringPaymentId = 'existing-recurring-payment-id',
30
- agreementId = 'agreement-id'
39
+ agreementId = 'agreement-id',
40
+ id = 'abc123'
31
41
  }) => ({
32
- params: { date, transactionId, paymentId, createdDate, existingRecurringPaymentId, agreementId }
42
+ params: { date, transactionId, paymentId, createdDate, existingRecurringPaymentId, agreementId, id }
33
43
  })
34
44
 
35
45
  const getMockResponseToolkit = () => ({
@@ -86,4 +96,26 @@ describe('recurring payments', () => {
86
96
  expect(recurringPayments[1].options.validate.params).toBe(processRPResultRequestParamsSchema)
87
97
  })
88
98
  })
99
+
100
+ describe('cancelRecurringPayment', () => {
101
+ it('handler should return continue response', async () => {
102
+ const request = getMockRequest({})
103
+ const responseToolkit = getMockResponseToolkit()
104
+ expect(await crpHandler(request, responseToolkit)).toEqual(responseToolkit.continue)
105
+ })
106
+
107
+ it('should call cancelRecurringPayment with id', async () => {
108
+ const id = Symbol('recurring-payment-id')
109
+ const request = getMockRequest({ id })
110
+ await crpHandler(request, getMockResponseToolkit())
111
+ expect(cancelRecurringPayment).toHaveBeenCalledWith(id)
112
+ })
113
+
114
+ it('should validate with cancelRecurringPaymentRequestParamsSchema', async () => {
115
+ const id = Symbol('recurring-payment-id')
116
+ const request = getMockRequest({ id })
117
+ await crpHandler(request, getMockResponseToolkit())
118
+ expect(recurringPayments[2].options.validate.params).toBe(cancelRecurringPaymentRequestParamsSchema)
119
+ })
120
+ })
89
121
  })
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  dueRecurringPaymentsRequestParamsSchema,
3
3
  dueRecurringPaymentsResponseSchema,
4
- processRPResultRequestParamsSchema
4
+ processRPResultRequestParamsSchema,
5
+ cancelRecurringPaymentRequestParamsSchema
5
6
  } from '../../schema/recurring-payments.schema.js'
6
- import { getRecurringPayments, processRPResult } from '../../services/recurring-payments.service.js'
7
+ import { getRecurringPayments, processRPResult, cancelRecurringPayment } from '../../services/recurring-payments.service.js'
7
8
 
8
9
  const SWAGGER_TAGS = ['api', 'recurring-payments']
9
10
 
@@ -55,5 +56,29 @@ export default [
55
56
  }
56
57
  }
57
58
  }
59
+ },
60
+ {
61
+ method: 'GET',
62
+ path: '/cancelRecurringPayment/{id}',
63
+ options: {
64
+ handler: async (request, h) => {
65
+ const { id } = request.params
66
+ const result = await cancelRecurringPayment(id)
67
+ return h.response(result)
68
+ },
69
+ description: 'Cancel a recurring payment',
70
+ tags: SWAGGER_TAGS,
71
+ validate: {
72
+ params: cancelRecurringPaymentRequestParamsSchema
73
+ },
74
+ plugins: {
75
+ 'hapi-swagger': {
76
+ responses: {
77
+ 200: { description: 'Recurring payment cancelled' }
78
+ },
79
+ order: 3
80
+ }
81
+ }
82
+ }
58
83
  }
59
84
  ]
@@ -5,9 +5,10 @@ Object {
5
5
  "agreementId": "435678",
6
6
  "cancelledDate": null,
7
7
  "cancelledReason": null,
8
- "endDate": 2023-11-12T00:00:00.000Z,
9
- "name": "Test Name",
10
- "nextDueDate": 2023-11-02T00:00:00.000Z,
8
+ "endDate": "2023-11-12T00:00:00.000Z",
9
+ "lastDigitsCardNumbers": "0128",
10
+ "name": "Fester Tester 2023",
11
+ "nextDueDate": "2023-11-02T00:00:00.000Z",
11
12
  "publicId": "abcdef99987",
12
13
  "status": 0,
13
14
  }
@@ -3,6 +3,7 @@ import {
3
3
  executeQuery,
4
4
  findDueRecurringPayments,
5
5
  findRecurringPaymentsByAgreementId,
6
+ findById,
6
7
  Permission,
7
8
  RecurringPayment
8
9
  } from '@defra-fish/dynamics-lib'
@@ -11,22 +12,38 @@ import {
11
12
  processRecurringPayment,
12
13
  generateRecurringPaymentRecord,
13
14
  processRPResult,
14
- findNewestExistingRecurringPaymentInCrm
15
+ findNewestExistingRecurringPaymentInCrm,
16
+ getRecurringPaymentAgreement,
17
+ cancelRecurringPayment
15
18
  } from '../recurring-payments.service.js'
16
19
  import { calculateEndDate, generatePermissionNumber } from '../permissions.service.js'
17
20
  import { getObfuscatedDob } from '../contacts.service.js'
18
21
  import { createHash } from 'node:crypto'
19
- import { AWS } from '@defra-fish/connectors-lib'
22
+ import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
20
23
  import { TRANSACTION_STAGING_TABLE, TRANSACTION_QUEUE } from '../../config.js'
21
24
  import { TRANSACTION_STATUS } from '../../services/transactions/constants.js'
22
25
  import { retrieveStagedTransaction } from '../../services/transactions/retrieve-transaction.js'
23
26
  import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../../services/paymentjournals/payment-journals.service.js'
24
27
  import { PAYMENT_JOURNAL_STATUS_CODES, TRANSACTION_SOURCE, PAYMENT_TYPE } from '@defra-fish/business-rules-lib'
28
+ import db from 'debug'
29
+
30
+ jest.mock('ioredis', () => ({
31
+ built: {
32
+ utils: {
33
+ debug: jest.fn()
34
+ }
35
+ }
36
+ }))
37
+
38
+ jest.mock('debug', () => jest.fn(() => jest.fn()))
39
+ const { value: debug } = db.mock.results[db.mock.calls.findIndex(c => c[0] === 'sales:recurring')]
40
+
25
41
  const { docClient, sqs } = AWS.mock.results[0].value
26
42
 
27
43
  jest.mock('@defra-fish/dynamics-lib', () => ({
28
44
  ...jest.requireActual('@defra-fish/dynamics-lib'),
29
45
  executeQuery: jest.fn(),
46
+ findById: jest.fn(),
30
47
  findDueRecurringPayments: jest.fn(),
31
48
  findRecurringPaymentsByAgreementId: jest.fn(() => ({ toRetrieveRequest: () => {} })),
32
49
  dynamicsClient: {
@@ -43,7 +60,10 @@ jest.mock('@defra-fish/connectors-lib', () => ({
43
60
  sqs: {
44
61
  sendMessage: jest.fn()
45
62
  }
46
- }))
63
+ })),
64
+ govUkPayApi: {
65
+ getRecurringPaymentAgreementInformation: jest.fn()
66
+ }
47
67
  }))
48
68
 
49
69
  jest.mock('node:crypto', () => ({
@@ -88,6 +108,7 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
88
108
  debit: Symbol('debit')
89
109
  }
90
110
  }))
111
+ global.structuredClone = obj => JSON.parse(JSON.stringify(obj))
91
112
 
92
113
  const getMockRecurringPayment = (overrides = {}) => ({
93
114
  name: 'Test Name',
@@ -224,7 +245,14 @@ const getMockResponse = () => ({
224
245
  })
225
246
 
226
247
  describe('recurring payments service', () => {
227
- const createSimpleSampleTransactionRecord = () => ({ payment: { recurring: true }, permissions: [{}] })
248
+ const createSimpleSampleTransactionRecord = () => ({
249
+ payment: {
250
+ recurring: {
251
+ nextDueDate: '2025-01-01T00:00:00.000Z'
252
+ }
253
+ },
254
+ permissions: [{}]
255
+ })
228
256
  const createSamplePermission = overrides => {
229
257
  const p = new Permission()
230
258
  p.referenceNumber = 'ABC123'
@@ -244,6 +272,11 @@ describe('recurring payments service', () => {
244
272
  beforeAll(() => {
245
273
  TRANSACTION_QUEUE.Url = 'TestUrl'
246
274
  TRANSACTION_STAGING_TABLE.TableName = 'TestTable'
275
+ const mockResponse = {
276
+ ok: true,
277
+ json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
278
+ }
279
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
247
280
  })
248
281
 
249
282
  describe('getRecurringPayments', () => {
@@ -281,13 +314,13 @@ describe('recurring payments service', () => {
281
314
  const transactionRecord = {
282
315
  payment: {
283
316
  recurring: {
284
- name: 'Test Name',
285
- nextDueDate: new Date('2023-11-02'),
317
+ nextDueDate: '2023-11-02T00:00:00.000Z',
286
318
  cancelledDate: null,
287
319
  cancelledReason: null,
288
- endDate: new Date('2023-11-12'),
320
+ endDate: '2023-11-12T00:00:00.000Z',
289
321
  agreementId: '435678',
290
- status: 0
322
+ status: 0,
323
+ last_digits_card_number: '0128'
291
324
  }
292
325
  },
293
326
  permissions: [getMockPermission()]
@@ -297,6 +330,19 @@ describe('recurring payments service', () => {
297
330
  expect(result.recurringPayment).toMatchSnapshot()
298
331
  })
299
332
 
333
+ it('should set a valid name on the recurringPayment', async () => {
334
+ const transactionRecord = {
335
+ payment: {
336
+ recurring: {
337
+ nextDueDate: '2023-07-07T00:00:00.000Z'
338
+ }
339
+ },
340
+ permissions: [getMockPermission()]
341
+ }
342
+ const result = await processRecurringPayment(transactionRecord, getMockContact())
343
+ expect(result.recurringPayment.name).toBe('Fester Tester 2023')
344
+ })
345
+
300
346
  it.each(['abc-123', 'def-987'])('generates a publicId %s for the recurring payment', async samplePublicId => {
301
347
  createHash.mockReturnValue({
302
348
  update: () => {},
@@ -333,7 +379,7 @@ describe('recurring payments service', () => {
333
379
  })
334
380
 
335
381
  describe('generateRecurringPaymentRecord', () => {
336
- const createFinalisedSampleTransaction = (agreementId, permission) => ({
382
+ const createFinalisedSampleTransaction = (agreementId, permission, lastDigitsCardNumbers) => ({
337
383
  expires: 1732892402,
338
384
  cost: 35.8,
339
385
  isRecurringPaymentSupported: true,
@@ -356,7 +402,8 @@ describe('recurring payments service', () => {
356
402
  id: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
357
403
  dataSource: 'Web Sales',
358
404
  transactionId: 'd26d646f-ed0f-4cf1-b6c1-ccfbbd611757',
359
- status: { id: 'FINALISED' }
405
+ status: { id: 'FINALISED' },
406
+ lastDigitsCardNumbers
360
407
  })
361
408
 
362
409
  it.each([
@@ -368,7 +415,8 @@ describe('recurring payments service', () => {
368
415
  issueDate: '2024-11-22T15:00:45.922Z',
369
416
  endDate: '2025-11-21T23:59:59.999Z'
370
417
  },
371
- '2025-11-12T00:00:00.000Z'
418
+ '2025-11-12T00:00:00.000Z',
419
+ '1234'
372
420
  ],
373
421
  [
374
422
  'next day start - next due on end date minus ten days',
@@ -378,7 +426,8 @@ describe('recurring payments service', () => {
378
426
  issueDate: '2024-11-22T15:00:45.922Z',
379
427
  endDate: '2025-11-22T23:59:59.999Z'
380
428
  },
381
- '2025-11-12T00:00:00.000Z'
429
+ '2025-11-12T00:00:00.000Z',
430
+ '5678'
382
431
  ],
383
432
  [
384
433
  'starts ten days after issue - next due on issue date plus one year',
@@ -388,7 +437,8 @@ describe('recurring payments service', () => {
388
437
  issueDate: '2024-11-12T15:00:45.922Z',
389
438
  endDate: '2025-11-21T23:59:59.999Z'
390
439
  },
391
- '2025-11-12T00:00:00.000Z'
440
+ '2025-11-12T00:00:00.000Z',
441
+ '9012'
392
442
  ],
393
443
  [
394
444
  'starts twenty days after issue - next due on issue date plus one year',
@@ -398,7 +448,8 @@ describe('recurring payments service', () => {
398
448
  issueDate: '2024-11-12T15:00:45.922Z',
399
449
  endDate: '2025-01-30T23:59:59.999Z'
400
450
  },
401
- '2025-11-12T00:00:00.000Z'
451
+ '2025-11-12T00:00:00.000Z',
452
+ '3456'
402
453
  ],
403
454
  [
404
455
  "issued on 29th Feb '24, starts on 30th March '24 - next due on 28th Feb '25",
@@ -408,7 +459,8 @@ describe('recurring payments service', () => {
408
459
  issueDate: '2024-02-29T12:38:24.123Z',
409
460
  endDate: '2025-03-29T23:59:59.999Z'
410
461
  },
411
- '2025-02-28T00:00:00.000Z'
462
+ '2025-02-28T00:00:00.000Z',
463
+ '7890'
412
464
  ],
413
465
  [
414
466
  "issued on 30th March '25 at 1am, starts at 1:30am - next due on 20th March '26",
@@ -418,13 +470,21 @@ describe('recurring payments service', () => {
418
470
  issueDate: '2025-03-30T01:00:00.000Z',
419
471
  endDate: '2026-03-29T23:59:59.999Z'
420
472
  },
421
- '2026-03-20T00:00:00.000Z'
473
+ '2026-03-20T00:00:00.000Z',
474
+ '1199'
422
475
  ]
423
- ])('creates record from transaction with %s', (_d, agreementId, permissionData, expectedNextDueDate) => {
424
- const sampleTransaction = createFinalisedSampleTransaction(agreementId, permissionData)
476
+ ])('creates record from transaction with %s', async (_d, agreementId, permissionData, expectedNextDueDate, lastDigitsCardNumbers) => {
477
+ const mockResponse = {
478
+ ok: true,
479
+ json: jest
480
+ .fn()
481
+ .mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: lastDigitsCardNumbers } } })
482
+ }
483
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
484
+ const sampleTransaction = createFinalisedSampleTransaction(agreementId, permissionData, lastDigitsCardNumbers)
425
485
  const permission = createSamplePermission(permissionData)
426
486
 
427
- const rpRecord = generateRecurringPaymentRecord(sampleTransaction, permission)
487
+ const rpRecord = await generateRecurringPaymentRecord(sampleTransaction, permission)
428
488
 
429
489
  expect(rpRecord).toEqual(
430
490
  expect.objectContaining({
@@ -436,7 +496,8 @@ describe('recurring payments service', () => {
436
496
  cancelledReason: null,
437
497
  endDate: permissionData.endDate,
438
498
  agreementId,
439
- status: 1
499
+ status: 1,
500
+ last_digits_card_number: lastDigitsCardNumbers
440
501
  })
441
502
  }),
442
503
  permissions: expect.arrayContaining([permission])
@@ -461,22 +522,26 @@ describe('recurring payments service', () => {
461
522
  endDate: '2025-11-10T23:59:59.999Z'
462
523
  }
463
524
  ]
464
- ])('throws an error for invalid dates when %s', (_d, permission) => {
525
+ ])('throws an error for invalid dates when %s', async (_d, permission) => {
465
526
  const sampleTransaction = createFinalisedSampleTransaction('hyu78ijhyu78ijuhyu78ij9iu6', permission)
466
527
 
467
- expect(() => generateRecurringPaymentRecord(sampleTransaction)).toThrow('Invalid dates provided for permission')
528
+ await expect(generateRecurringPaymentRecord(sampleTransaction)).rejects.toThrow('Invalid dates provided for permission')
468
529
  })
469
530
 
470
- it('returns a false flag when agreementId is not present', () => {
471
- const sampleTransaction = createFinalisedSampleTransaction(null, {
472
- startDate: '2024-11-22T15:30:45.922Z',
473
- issueDate: '2024-11-22T15:00:45.922Z',
474
- endDate: '2025-11-21T23:59:59.999Z'
475
- })
531
+ it('returns a false flag when agreementId is not present', async () => {
532
+ const sampleTransaction = createFinalisedSampleTransaction(
533
+ null,
534
+ {
535
+ startDate: '2024-11-22T15:30:45.922Z',
536
+ issueDate: '2024-11-22T15:00:45.922Z',
537
+ endDate: '2025-11-21T23:59:59.999Z'
538
+ },
539
+ '0123'
540
+ )
476
541
 
477
- const rpRecord = generateRecurringPaymentRecord(sampleTransaction)
542
+ const rpRecord = await generateRecurringPaymentRecord(sampleTransaction)
478
543
 
479
- expect(rpRecord.payment.recurring).toBeFalsy()
544
+ expect(rpRecord.payment?.recurring).toBeFalsy()
480
545
  })
481
546
  })
482
547
 
@@ -733,4 +798,89 @@ describe('recurring payments service', () => {
733
798
  expect(rcp).toBeFalsy()
734
799
  })
735
800
  })
801
+
802
+ describe('getRecurringPaymentAgreement', () => {
803
+ const agreementId = '1234'
804
+
805
+ it('should send provided agreement id data to Gov.UK Pay', async () => {
806
+ const mockResponse = {
807
+ ok: true,
808
+ json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
809
+ }
810
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
811
+ await getRecurringPaymentAgreement(agreementId)
812
+ expect(govUkPayApi.getRecurringPaymentAgreementInformation).toHaveBeenCalledWith(agreementId)
813
+ })
814
+
815
+ it('should return response body when payment creation is successful', async () => {
816
+ const mockResponse = {
817
+ ok: true,
818
+ json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
819
+ }
820
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
821
+
822
+ const result = await getRecurringPaymentAgreement(agreementId)
823
+
824
+ expect(result).toEqual({
825
+ success: true,
826
+ payment_instrument: {
827
+ card_details: {
828
+ last_digits_card_number: '1234'
829
+ }
830
+ }
831
+ })
832
+ })
833
+
834
+ it('debug should output message when response.ok is true without card details', async () => {
835
+ const mockResponse = {
836
+ ok: true,
837
+ json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
838
+ }
839
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
840
+
841
+ await getRecurringPaymentAgreement(agreementId)
842
+
843
+ expect(debug).toHaveBeenCalledWith('Successfully got recurring payment agreement information: %o', {
844
+ success: true,
845
+ payment_instrument: {}
846
+ })
847
+ })
848
+
849
+ it('should throw an error when the response is not ok', async () => {
850
+ const mockResponse = {
851
+ ok: false,
852
+ json: jest.fn().mockResolvedValue({ success: true, payment_instrument: { card_details: { last_digits_card_number: '1234' } } })
853
+ }
854
+ govUkPayApi.getRecurringPaymentAgreementInformation.mockResolvedValue(mockResponse)
855
+
856
+ await expect(getRecurringPaymentAgreement(agreementId)).rejects.toThrow('Failure getting agreement in the GOV.UK API service')
857
+ })
858
+ })
859
+
860
+ describe('cancelRecurringPayment', () => {
861
+ it('should call findById with RecurringPayment and the provided id', async () => {
862
+ const id = 'abc123'
863
+ await cancelRecurringPayment(id)
864
+ expect(findById).toHaveBeenCalledWith(RecurringPayment, id)
865
+ })
866
+
867
+ it('should log a RecurringPayment record when there is one match', async () => {
868
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
869
+ const recurringPayment = { entity: getMockRecurringPayment() }
870
+ findById.mockReturnValueOnce(recurringPayment)
871
+
872
+ await cancelRecurringPayment('id')
873
+
874
+ expect(consoleLogSpy).toHaveBeenCalledWith('RecurringPayment for cancellation: ', recurringPayment)
875
+ })
876
+
877
+ it('should log no matches when there are no matches', async () => {
878
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
879
+ findById.mockReturnValueOnce(undefined)
880
+
881
+ await cancelRecurringPayment('id')
882
+
883
+ expect(consoleLogSpy).toHaveBeenCalledWith('No matches found for cancellation')
884
+ })
885
+ })
736
886
  })
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  executeQuery,
3
+ findById,
3
4
  findDueRecurringPayments,
4
5
  findRecurringPaymentsByAgreementId,
5
6
  RecurringPayment,
@@ -14,7 +15,9 @@ import { TRANSACTION_STATUS } from '../services/transactions/constants.js'
14
15
  import { retrieveStagedTransaction } from '../services/transactions/retrieve-transaction.js'
15
16
  import { createPaymentJournal, getPaymentJournal, updatePaymentJournal } from '../services/paymentjournals/payment-journals.service.js'
16
17
  import moment from 'moment'
17
- import { AWS } from '@defra-fish/connectors-lib'
18
+ import { AWS, govUkPayApi } from '@defra-fish/connectors-lib'
19
+ import db from 'debug'
20
+ const debug = db('sales:recurring')
18
21
  const { sqs, docClient } = AWS()
19
22
 
20
23
  export const getRecurringPayments = date => executeQuery(findDueRecurringPayments(date))
@@ -33,8 +36,10 @@ const getNextDueDate = (startDate, issueDate, endDate) => {
33
36
  throw new Error('Invalid dates provided for permission')
34
37
  }
35
38
 
36
- export const generateRecurringPaymentRecord = (transactionRecord, permission) => {
39
+ export const generateRecurringPaymentRecord = async (transactionRecord, permission) => {
37
40
  if (transactionRecord.agreementId) {
41
+ const agreementResponse = await getRecurringPaymentAgreement(transactionRecord.agreementId)
42
+ const lastDigitsCardNumbers = agreementResponse.payment_instrument?.card_details?.last_digits_card_number
38
43
  const [{ startDate, issueDate, endDate }] = transactionRecord.permissions
39
44
  return {
40
45
  payment: {
@@ -45,7 +50,8 @@ export const generateRecurringPaymentRecord = (transactionRecord, permission) =>
45
50
  cancelledReason: null,
46
51
  endDate,
47
52
  agreementId: transactionRecord.agreementId,
48
- status: 1
53
+ status: 1,
54
+ last_digits_card_number: lastDigitsCardNumbers
49
55
  }
50
56
  },
51
57
  permissions: [permission]
@@ -64,7 +70,7 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
64
70
  if (transactionRecord.payment?.recurring) {
65
71
  const recurringPayment = new RecurringPayment()
66
72
  hash.update(recurringPayment.uniqueContentId)
67
- recurringPayment.name = transactionRecord.payment.recurring.name
73
+ recurringPayment.name = determineRecurringPaymentName(transactionRecord, contact)
68
74
  recurringPayment.nextDueDate = transactionRecord.payment.recurring.nextDueDate
69
75
  recurringPayment.cancelledDate = transactionRecord.payment.recurring.cancelledDate
70
76
  recurringPayment.cancelledReason = transactionRecord.payment.recurring.cancelledReason
@@ -72,6 +78,7 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
72
78
  recurringPayment.agreementId = transactionRecord.payment.recurring.agreementId
73
79
  recurringPayment.publicId = hash.digest('base64')
74
80
  recurringPayment.status = transactionRecord.payment.recurring.status
81
+ recurringPayment.lastDigitsCardNumbers = transactionRecord.payment.recurring.last_digits_card_number
75
82
  const [permission] = transactionRecord.permissions
76
83
  recurringPayment.bindToEntity(RecurringPayment.definition.relationships.activePermission, permission)
77
84
  recurringPayment.bindToEntity(RecurringPayment.definition.relationships.contact, contact)
@@ -80,6 +87,22 @@ export const processRecurringPayment = async (transactionRecord, contact) => {
80
87
  return { recurringPayment: null }
81
88
  }
82
89
 
90
+ export const getRecurringPaymentAgreement = async agreementId => {
91
+ const response = await govUkPayApi.getRecurringPaymentAgreementInformation(agreementId)
92
+ if (response.ok) {
93
+ const resBody = await response.json()
94
+ const resBodyNoCardDetails = structuredClone(resBody)
95
+
96
+ if (resBodyNoCardDetails.payment_instrument?.card_details) {
97
+ delete resBodyNoCardDetails.payment_instrument.card_details
98
+ }
99
+ debug('Successfully got recurring payment agreement information: %o', resBodyNoCardDetails)
100
+ return resBody
101
+ } else {
102
+ throw new Error('Failure getting agreement in the GOV.UK API service')
103
+ }
104
+ }
105
+
83
106
  export const processRPResult = async (transactionId, paymentId, createdDate) => {
84
107
  const transactionRecord = await retrieveStagedTransaction(transactionId)
85
108
  if (await getPaymentJournal(transactionId)) {
@@ -138,3 +161,17 @@ export const findNewestExistingRecurringPaymentInCrm = async agreementId => {
138
161
  }
139
162
  return false
140
163
  }
164
+
165
+ export const cancelRecurringPayment = async id => {
166
+ const recurringPayment = await findById(RecurringPayment, id)
167
+ if (recurringPayment) {
168
+ console.log('RecurringPayment for cancellation: ', recurringPayment)
169
+ } else {
170
+ console.log('No matches found for cancellation')
171
+ }
172
+ }
173
+
174
+ const determineRecurringPaymentName = (transactionRecord, contact) => {
175
+ const [dueYear] = transactionRecord.payment.recurring.nextDueDate.split('-')
176
+ return [contact.firstName, contact.lastName, dueYear].join(' ')
177
+ }
@@ -77,7 +77,7 @@ export async function processQueue ({ id }) {
77
77
 
78
78
  entities.push(contact, permission)
79
79
 
80
- const { recurringPayment } = await processRecurringPayment(generateRecurringPaymentRecord(transactionRecord, permission), contact)
80
+ const { recurringPayment } = await processRecurringPayment(await generateRecurringPaymentRecord(transactionRecord, permission), contact)
81
81
 
82
82
  if (recurringPayment && permit.isRecurringPaymentSupported) {
83
83
  entities.push(recurringPayment)