@defra-fish/recurring-payments-job 1.59.0-rc.3 → 1.59.0-rc.5
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
|
+
"version": "1.59.0-rc.5",
|
|
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.
|
|
40
|
-
"@defra-fish/connectors-lib": "1.59.0-rc.
|
|
39
|
+
"@defra-fish/business-rules-lib": "1.59.0-rc.5",
|
|
40
|
+
"@defra-fish/connectors-lib": "1.59.0-rc.5",
|
|
41
41
|
"commander": "^7.2.0",
|
|
42
42
|
"moment-timezone": "^0.5.34"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "a14e3194db1d9f75d1eb49ef3e873f442970fcf5"
|
|
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 =
|
|
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 = '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|