@defra-fish/recurring-payments-job 1.59.0-rc.3 → 1.59.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/recurring-payments-job",
3
- "version": "1.59.0-rc.3",
3
+ "version": "1.59.0-rc.4",
4
4
  "description": "Rod Licensing Recurring Payments Job",
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,10 +36,10 @@
36
36
  "test": "echo \"Error: run tests from root\" && exit 1"
37
37
  },
38
38
  "dependencies": {
39
- "@defra-fish/business-rules-lib": "1.59.0-rc.3",
40
- "@defra-fish/connectors-lib": "1.59.0-rc.3",
39
+ "@defra-fish/business-rules-lib": "1.59.0-rc.4",
40
+ "@defra-fish/connectors-lib": "1.59.0-rc.4",
41
41
  "commander": "^7.2.0",
42
42
  "moment-timezone": "^0.5.34"
43
43
  },
44
- "gitHead": "ba5b2afbe6d3c096d813ce3445c39db2c088523d"
44
+ "gitHead": "5e8bd6ced8acd15794692b128dbac03afacd2d98"
45
45
  }
@@ -1,6 +1,6 @@
1
1
  import { salesApi } from '@defra-fish/connectors-lib'
2
2
  import { processRecurringPayments } from '../recurring-payments-processor.js'
3
- import { sendPayment } from '../services/govuk-pay-service.js'
3
+ import { getPaymentStatus, sendPayment } from '../services/govuk-pay-service.js'
4
4
 
5
5
  jest.mock('@defra-fish/business-rules-lib')
6
6
  jest.mock('@defra-fish/connectors-lib', () => ({
@@ -14,14 +14,19 @@ jest.mock('@defra-fish/connectors-lib', () => ({
14
14
  }))
15
15
  }
16
16
  }))
17
+
17
18
  jest.mock('../services/govuk-pay-service.js', () => ({
18
- sendPayment: jest.fn()
19
+ sendPayment: jest.fn(),
20
+ getPaymentStatus: jest.fn()
19
21
  }))
20
22
 
23
+ const PAYMENT_STATUS_DELAY = 60000
24
+
21
25
  describe('recurring-payments-processor', () => {
22
26
  beforeEach(() => {
23
27
  jest.clearAllMocks()
24
28
  process.env.RUN_RECURRING_PAYMENTS = 'true'
29
+ global.setTimeout = jest.fn((cb, ms) => cb())
25
30
  })
26
31
 
27
32
  it('console log displays "Recurring Payments job disabled" when env is false', async () => {
@@ -62,6 +67,7 @@ describe('recurring-payments-processor', () => {
62
67
  salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment(referenceNumber)])
63
68
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
64
69
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
70
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
65
71
 
66
72
  await processRecurringPayments()
67
73
 
@@ -110,8 +116,9 @@ describe('recurring-payments-processor', () => {
110
116
  ]
111
117
  }
112
118
 
113
- const mockPaymentResponse = { payment_id: 'test-payment-id' }
119
+ const mockPaymentResponse = { payment_id: 'test-payment-id', agreementId: 'test-agreement-id' }
114
120
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
121
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
115
122
 
116
123
  await processRecurringPayments()
117
124
 
@@ -136,6 +143,7 @@ describe('recurring-payments-processor', () => {
136
143
 
137
144
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
138
145
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
146
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
139
147
 
140
148
  await processRecurringPayments()
141
149
 
@@ -165,6 +173,7 @@ describe('recurring-payments-processor', () => {
165
173
 
166
174
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
167
175
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
176
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
168
177
 
169
178
  await processRecurringPayments()
170
179
 
@@ -185,6 +194,7 @@ describe('recurring-payments-processor', () => {
185
194
 
186
195
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
187
196
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
197
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
188
198
 
189
199
  await processRecurringPayments()
190
200
 
@@ -207,7 +217,7 @@ describe('recurring-payments-processor', () => {
207
217
 
208
218
  it('prepares and sends the payment request', async () => {
209
219
  const agreementId = Symbol('agreementId')
210
- const transactionId = Symbol('transactionId')
220
+ const transactionId = 'transactionId'
211
221
 
212
222
  salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment('foo', agreementId)])
213
223
 
@@ -220,8 +230,9 @@ describe('recurring-payments-processor', () => {
220
230
  id: transactionId
221
231
  })
222
232
 
223
- const mockPaymentResponse = { payment_id: 'test-payment-id' }
233
+ const mockPaymentResponse = { payment_id: 'test-payment-id', agreementId }
224
234
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
235
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
225
236
 
226
237
  const expectedData = {
227
238
  amount: 5000,
@@ -236,6 +247,88 @@ describe('recurring-payments-processor', () => {
236
247
  expect(sendPayment).toHaveBeenCalledWith(expectedData)
237
248
  })
238
249
 
250
+ it('should call getPaymentStatus with payment id', async () => {
251
+ const mockResponse = [
252
+ {
253
+ entity: { agreementId: 'agreement-1' },
254
+ expanded: {
255
+ activePermission: {
256
+ entity: {
257
+ referenceNumber: 'ref-1'
258
+ }
259
+ }
260
+ }
261
+ }
262
+ ]
263
+ salesApi.getDueRecurringPayments.mockResolvedValue(mockResponse)
264
+ salesApi.createTransaction.mockResolvedValue({
265
+ id: 'payment-id-1'
266
+ })
267
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
268
+ const mockPaymentResponse = { payment_id: 'test-payment-id', agreementId: 'agreement-1' }
269
+ sendPayment.mockResolvedValueOnce(mockPaymentResponse)
270
+
271
+ await processRecurringPayments()
272
+ jest.advanceTimersByTime(60000)
273
+
274
+ expect(getPaymentStatus).toHaveBeenCalledWith('test-payment-id')
275
+ })
276
+
277
+ it('should log payment status for recurring payment', async () => {
278
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(jest.fn())
279
+ const mockPaymentId = 'test-payment-id'
280
+ const mockResponse = [
281
+ {
282
+ entity: { agreementId: 'agreement-1' },
283
+ expanded: {
284
+ activePermission: {
285
+ entity: {
286
+ referenceNumber: 'ref-1'
287
+ }
288
+ }
289
+ }
290
+ }
291
+ ]
292
+ salesApi.getDueRecurringPayments.mockResolvedValue(mockResponse)
293
+ salesApi.createTransaction.mockResolvedValue({
294
+ id: mockPaymentId
295
+ })
296
+ const mockPaymentResponse = { payment_id: mockPaymentId, agreementId: 'agreement-1' }
297
+ sendPayment.mockResolvedValueOnce(mockPaymentResponse)
298
+ const mockPaymentStatus = { state: { status: 'Success' } }
299
+ getPaymentStatus.mockResolvedValueOnce(mockPaymentStatus)
300
+ const mockStatus = JSON.stringify(mockPaymentStatus.state.status)
301
+
302
+ await processRecurringPayments()
303
+ jest.advanceTimersByTime(60000)
304
+
305
+ expect(consoleLogSpy).toHaveBeenCalledWith(`Payment status for ${mockPaymentId}: ${mockStatus}`)
306
+ })
307
+
308
+ it('should call setTimeout with correct delay when there are recurring payments', async () => {
309
+ const referenceNumber = Symbol('reference')
310
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment(referenceNumber)])
311
+ const mockPaymentResponse = { payment_id: 'test-payment-id' }
312
+ sendPayment.mockResolvedValueOnce(mockPaymentResponse)
313
+ getPaymentStatus.mockResolvedValueOnce({ state: { status: 'Success' } })
314
+
315
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
316
+
317
+ await processRecurringPayments()
318
+
319
+ expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), PAYMENT_STATUS_DELAY)
320
+ })
321
+
322
+ it('should not call setTimeout when there are no recurring payments', async () => {
323
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce([])
324
+
325
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
326
+
327
+ await processRecurringPayments()
328
+
329
+ expect(setTimeoutSpy).not.toHaveBeenCalled()
330
+ })
331
+
239
332
  describe.each([2, 3, 10])('if there are %d recurring payments', count => {
240
333
  it('prepares the data for each one', async () => {
241
334
  const references = []
@@ -250,6 +343,8 @@ describe('recurring-payments-processor', () => {
250
343
  salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
251
344
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
252
345
  sendPayment.mockResolvedValue(mockPaymentResponse)
346
+ const mockPaymentStatus = { state: { status: 'Success' } }
347
+ getPaymentStatus.mockResolvedValue(mockPaymentStatus)
253
348
 
254
349
  const expectedData = []
255
350
  references.forEach(reference => {
@@ -337,10 +432,50 @@ describe('recurring-payments-processor', () => {
337
432
  await processRecurringPayments()
338
433
  expect(sendPayment.mock.calls).toEqual(expectedData)
339
434
  })
435
+
436
+ it('gets the payment status for each one', async () => {
437
+ const mockGetDueRecurringPayments = []
438
+ const agreementIds = []
439
+ for (let i = 0; i < count; i++) {
440
+ const agreementId = Symbol(`agreementId${1}`)
441
+ agreementIds.push(agreementId)
442
+ mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i, agreementId))
443
+ }
444
+ salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
445
+
446
+ const permits = []
447
+ for (let i = 0; i < count; i++) {
448
+ permits.push(Symbol(`permit${i}`))
449
+ }
450
+
451
+ permits.forEach((permit, i) => {
452
+ salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
453
+ licensee: { countryCode: 'GB-ENG' }
454
+ })
455
+
456
+ salesApi.createTransaction.mockReturnValueOnce({
457
+ cost: i,
458
+ id: permit
459
+ })
460
+ })
461
+
462
+ const expectedData = []
463
+ permits.forEach((_, index) => {
464
+ const paymentId = `payment-id-${index}`
465
+ expectedData.push(paymentId)
466
+ const mockPaymentResponse = { payment_id: paymentId }
467
+ sendPayment.mockResolvedValueOnce(mockPaymentResponse)
468
+ })
469
+
470
+ await processRecurringPayments()
471
+ expectedData.forEach(paymentId => {
472
+ expect(getPaymentStatus).toHaveBeenCalledWith(paymentId)
473
+ })
474
+ })
340
475
  })
341
476
  })
342
477
 
343
- const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = '456') => ({
478
+ const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = 'test-agreement-id') => ({
344
479
  entity: { agreementId },
345
480
  expanded: { activePermission: { entity: { referenceNumber } } }
346
481
  })
@@ -1,8 +1,9 @@
1
1
  import moment from 'moment-timezone'
2
2
  import { SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
3
3
  import { salesApi } from '@defra-fish/connectors-lib'
4
- import { sendPayment } from './services/govuk-pay-service.js'
4
+ import { getPaymentStatus, sendPayment } from './services/govuk-pay-service.js'
5
5
 
6
+ const PAYMENT_STATUS_DELAY = 60000
6
7
  const payments = []
7
8
 
8
9
  export const processRecurringPayments = async () => {
@@ -12,7 +13,10 @@ export const processRecurringPayments = async () => {
12
13
  const response = await salesApi.getDueRecurringPayments(date)
13
14
  console.log('Recurring Payments found: ', response)
14
15
  await Promise.all(response.map(record => processRecurringPayment(record)))
15
- console.log('Recurring Payments processed:', payments)
16
+ if (response.length > 0) {
17
+ await new Promise(resolve => setTimeout(resolve, PAYMENT_STATUS_DELAY))
18
+ await Promise.all(response.map(record => processRecurringPaymentStatus(record)))
19
+ }
16
20
  } else {
17
21
  console.log('Recurring Payments job disabled')
18
22
  }
@@ -89,3 +93,17 @@ const preparePayment = (agreementId, transaction) => {
89
93
 
90
94
  return result
91
95
  }
96
+
97
+ const processRecurringPaymentStatus = async record => {
98
+ const agreementId = record.entity.agreementId
99
+ const paymentId = getPaymentId(agreementId)
100
+ const {
101
+ state: { status }
102
+ } = await getPaymentStatus(paymentId)
103
+ console.log(`Payment status for ${paymentId}: ${JSON.stringify(status)}`)
104
+ }
105
+
106
+ const getPaymentId = agreementId => {
107
+ const payment = payments.find(p => p.agreementId === agreementId)
108
+ return payment.paymentId
109
+ }
@@ -1,4 +1,4 @@
1
- import { sendPayment } from '../govuk-pay-service.js'
1
+ import { getPaymentStatus, sendPayment } from '../govuk-pay-service.js'
2
2
  import { govUkPayApi } from '@defra-fish/connectors-lib'
3
3
 
4
4
  jest.mock('@defra-fish/connectors-lib')
@@ -32,7 +32,7 @@ describe('govuk-pay-service', () => {
32
32
  description: 'The recurring card payment for your rod fishing licence',
33
33
  reference: unique
34
34
  }
35
- await sendPayment(payload)
35
+ sendPayment(payload)
36
36
  expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, true)
37
37
  })
38
38
 
@@ -61,4 +61,60 @@ describe('govuk-pay-service', () => {
61
61
  }
62
62
  })
63
63
  })
64
+
65
+ describe('getPaymentStatus', () => {
66
+ it('should call fetchPaymentStatus with payment id and true for recurring payments', async () => {
67
+ govUkPayApi.fetchPaymentStatus.mockResolvedValue({
68
+ ok: true,
69
+ json: jest.fn().mockResolvedValue({ code: 'P1234', description: 'Success' })
70
+ })
71
+ const paymentId = Symbol('transactionId')
72
+ await getPaymentStatus(paymentId)
73
+ expect(govUkPayApi.fetchPaymentStatus).toHaveBeenCalledWith(paymentId, true)
74
+ })
75
+
76
+ it('should return the payment status on successful response', async () => {
77
+ const mockPaymentStatus = { code: 'P1234', description: 'Success' }
78
+ govUkPayApi.fetchPaymentStatus.mockResolvedValue({
79
+ ok: true,
80
+ json: jest.fn().mockResolvedValue(mockPaymentStatus)
81
+ })
82
+ const paymentId = 'valid-payment-id'
83
+ const result = await getPaymentStatus(paymentId)
84
+ expect(result).toEqual(mockPaymentStatus)
85
+ })
86
+
87
+ it('should throw an error when payment ID is not provided', async () => {
88
+ await expect(getPaymentStatus(null)).rejects.toThrow('Invalid payment ID')
89
+ })
90
+
91
+ it('should throw an error when response is not ok', async () => {
92
+ const mockErrorDetails = { error: 'Payment not found' }
93
+ const mockFetchResponse = {
94
+ ok: false,
95
+ json: jest.fn().mockResolvedValue(mockErrorDetails)
96
+ }
97
+ govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockFetchResponse)
98
+
99
+ await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Payment not found')
100
+ })
101
+
102
+ it('should throw an error when response is not ok but errorDetails has no value', async () => {
103
+ const mockErrorDetails = {}
104
+ const mockFetchResponse = {
105
+ ok: false,
106
+ json: jest.fn().mockResolvedValue(mockErrorDetails)
107
+ }
108
+ govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockFetchResponse)
109
+
110
+ await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Error fetching payment status')
111
+ })
112
+
113
+ it('should throw an error when fetchPaymentStatus fails', async () => {
114
+ const mockError = new Error('Network error')
115
+ govUkPayApi.fetchPaymentStatus.mockRejectedValue(mockError)
116
+
117
+ await expect(getPaymentStatus('test-payment-id')).rejects.toThrow('Network error')
118
+ })
119
+ })
64
120
  })
@@ -9,3 +9,25 @@ export const sendPayment = async preparedPayment => {
9
9
  throw e
10
10
  }
11
11
  }
12
+
13
+ export const getPaymentStatus = async paymentId => {
14
+ if (!paymentId) {
15
+ throw new Error('Invalid payment ID')
16
+ }
17
+
18
+ try {
19
+ const response = await govUkPayApi.fetchPaymentStatus(paymentId, true)
20
+
21
+ if (!response.ok) {
22
+ const errorDetails = await response.json()
23
+ console.log(errorDetails)
24
+ throw new Error(errorDetails.error || 'Error fetching payment status')
25
+ }
26
+
27
+ const paymentStatus = await response.json()
28
+ return paymentStatus
29
+ } catch (error) {
30
+ console.error('Error in getPaymentStatus:', error)
31
+ throw error
32
+ }
33
+ }