@defra-fish/recurring-payments-job 1.63.0-rc.8 → 1.63.0
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.63.0
|
|
3
|
+
"version": "1.63.0",
|
|
4
4
|
"description": "Rod Licensing Recurring Payments Job",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@defra-fish/business-rules-lib": "1.63.0
|
|
40
|
-
"@defra-fish/connectors-lib": "1.63.0
|
|
39
|
+
"@defra-fish/business-rules-lib": "1.63.0",
|
|
40
|
+
"@defra-fish/connectors-lib": "1.63.0",
|
|
41
41
|
"commander": "^7.2.0",
|
|
42
42
|
"debug": "^4.3.3",
|
|
43
43
|
"moment-timezone": "^0.5.34"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "7cfb8ef668002fc340b755dd3aaa4572063e115c"
|
|
46
46
|
}
|
|
@@ -1,49 +1,66 @@
|
|
|
1
1
|
import commander from 'commander'
|
|
2
|
-
import {
|
|
2
|
+
import { execute } from '../recurring-payments-processor.js'
|
|
3
|
+
import fs from 'fs'
|
|
3
4
|
|
|
4
5
|
jest.useFakeTimers()
|
|
5
|
-
|
|
6
|
-
jest.mock('
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
jest.mock('../recurring-payments-processor.js')
|
|
7
|
+
jest.mock('fs', () => ({
|
|
8
|
+
readFileSync: jest.fn(),
|
|
9
|
+
promises: {
|
|
10
|
+
readFile: jest.fn()
|
|
9
11
|
}
|
|
10
|
-
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
jest.mock('commander', () => {
|
|
14
|
-
if (!global.commander) {
|
|
15
|
-
global.commander = jest.requireActual('commander')
|
|
16
|
-
}
|
|
17
|
-
return global.commander
|
|
18
|
-
})
|
|
12
|
+
}))
|
|
19
13
|
|
|
20
14
|
describe('recurring-payments-job', () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
fs.readFileSync.mockReturnValue(JSON.stringify({ name: 'recurring-payments-test', version: '1.0.0' }))
|
|
17
|
+
})
|
|
18
|
+
|
|
21
19
|
beforeEach(() => {
|
|
22
20
|
jest.clearAllMocks()
|
|
23
21
|
commander.args = ['test']
|
|
24
22
|
})
|
|
25
23
|
|
|
26
|
-
it('
|
|
24
|
+
it('logs startup details including name and version', () => {
|
|
25
|
+
const mockPkg = { name: 'recurring-payments-test', version: '1.2.3' }
|
|
26
|
+
fs.readFileSync.mockReturnValueOnce(JSON.stringify(mockPkg))
|
|
27
|
+
|
|
28
|
+
jest.isolateModules(() => {
|
|
29
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
|
30
|
+
require('../recurring-payments-job.js')
|
|
31
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
32
|
+
'Recurring payments job starting at %s. name: %s. version: %s',
|
|
33
|
+
expect.any(String),
|
|
34
|
+
mockPkg.name,
|
|
35
|
+
mockPkg.version
|
|
36
|
+
)
|
|
37
|
+
logSpy.mockRestore()
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('calls execute when no delay', () => {
|
|
27
42
|
jest.isolateModules(() => {
|
|
43
|
+
process.env.RECURRING_PAYMENTS_LOCAL_DELAY = '0'
|
|
28
44
|
require('../recurring-payments-job.js')
|
|
29
|
-
expect(
|
|
45
|
+
expect(execute).toHaveBeenCalled()
|
|
30
46
|
})
|
|
31
47
|
})
|
|
32
48
|
|
|
33
49
|
it('doesnt call setTimeout when no correct delay', () => {
|
|
34
50
|
jest.isolateModules(() => {
|
|
51
|
+
process.env.RECURRING_PAYMENTS_LOCAL_DELAY = 'invalid-delay'
|
|
35
52
|
const setTimeoutSpy = jest.spyOn(global, 'setTimeout')
|
|
36
53
|
require('../recurring-payments-job.js')
|
|
37
54
|
expect(setTimeoutSpy).not.toHaveBeenCalled()
|
|
38
55
|
})
|
|
39
56
|
})
|
|
40
57
|
|
|
41
|
-
it('calls
|
|
58
|
+
it('calls execute when delay', () => {
|
|
42
59
|
process.env.RECURRING_PAYMENTS_LOCAL_DELAY = '5'
|
|
43
60
|
jest.isolateModules(() => {
|
|
44
61
|
require('../recurring-payments-job.js')
|
|
45
62
|
jest.advanceTimersByTime(parseInt(process.env.RECURRING_PAYMENTS_LOCAL_DELAY) * 1000)
|
|
46
|
-
expect(
|
|
63
|
+
expect(execute).toHaveBeenCalled()
|
|
47
64
|
})
|
|
48
65
|
})
|
|
49
66
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { salesApi } from '@defra-fish/connectors-lib'
|
|
1
|
+
import { airbrake, salesApi } from '@defra-fish/connectors-lib'
|
|
2
2
|
import { PAYMENT_STATUS, PAYMENT_JOURNAL_STATUS_CODES } from '@defra-fish/business-rules-lib'
|
|
3
|
-
import {
|
|
3
|
+
import { execute } from '../recurring-payments-processor.js'
|
|
4
4
|
import { getPaymentStatus, isGovPayUp, sendPayment } from '../services/govuk-pay-service.js'
|
|
5
5
|
import db from 'debug'
|
|
6
6
|
|
|
@@ -19,18 +19,24 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
|
|
|
19
19
|
}
|
|
20
20
|
}))
|
|
21
21
|
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
22
|
+
airbrake: {
|
|
23
|
+
initialise: jest.fn(),
|
|
24
|
+
flush: jest.fn()
|
|
25
|
+
},
|
|
22
26
|
salesApi: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
licensee: { countryCode: 'GB-ENG' }
|
|
26
|
-
})),
|
|
27
|
+
cancelRecurringPayment: jest.fn(),
|
|
28
|
+
createPaymentJournal: jest.fn(),
|
|
27
29
|
createTransaction: jest.fn(() => ({
|
|
28
30
|
id: 'test-transaction-id',
|
|
29
31
|
cost: 30
|
|
30
32
|
})),
|
|
33
|
+
getDueRecurringPayments: jest.fn(() => []),
|
|
34
|
+
getPaymentJournal: jest.fn(),
|
|
35
|
+
preparePermissionDataForRenewal: jest.fn(() => ({
|
|
36
|
+
licensee: { countryCode: 'GB-ENG' }
|
|
37
|
+
})),
|
|
31
38
|
processRPResult: jest.fn(),
|
|
32
|
-
updatePaymentJournal: jest.fn()
|
|
33
|
-
getPaymentJournal: jest.fn()
|
|
39
|
+
updatePaymentJournal: jest.fn()
|
|
34
40
|
}
|
|
35
41
|
}))
|
|
36
42
|
|
|
@@ -73,49 +79,120 @@ describe('recurring-payments-processor', () => {
|
|
|
73
79
|
global.setTimeout = jest.fn((cb, ms) => cb())
|
|
74
80
|
})
|
|
75
81
|
|
|
82
|
+
it('initialises airbrake', () => {
|
|
83
|
+
jest.isolateModules(async () => {
|
|
84
|
+
require('../recurring-payments-processor.js')
|
|
85
|
+
await execute()
|
|
86
|
+
expect(airbrake.initialise).toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('flushes airbrake before script ends', () => {
|
|
91
|
+
jest.isolateModules(async () => {
|
|
92
|
+
const { execute } = require('../recurring-payments-processor.js')
|
|
93
|
+
await execute()
|
|
94
|
+
expect(airbrake.flush).toHaveBeenCalled()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("doesn't flush airbrake before execute has been called", () => {
|
|
99
|
+
jest.isolateModules(() => {
|
|
100
|
+
require('../recurring-payments-processor.js')
|
|
101
|
+
expect(airbrake.flush).not.toHaveBeenCalled()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it.each([
|
|
106
|
+
['SIGINT', 130],
|
|
107
|
+
['SIGTERM', 137]
|
|
108
|
+
])('flushes airbrake on %s signal', (signal, code) => {
|
|
109
|
+
jest.isolateModules(() => {
|
|
110
|
+
// setup a delay so script doesn't call processRecurringPayments and exit naturally
|
|
111
|
+
process.env.RECURRING_PAYMENTS_LOCAL_DELAY = '1'
|
|
112
|
+
const signalCallbacks = {}
|
|
113
|
+
jest.spyOn(process, 'on')
|
|
114
|
+
jest.spyOn(process, 'exit')
|
|
115
|
+
process.on.mockImplementation((signalToken, callback) => {
|
|
116
|
+
signalCallbacks[signalToken] = callback
|
|
117
|
+
})
|
|
118
|
+
process.exit.mockImplementation(() => {
|
|
119
|
+
// so we don't crash out of the tests!
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
require('../recurring-payments-processor.js')
|
|
123
|
+
signalCallbacks[signal]()
|
|
124
|
+
|
|
125
|
+
expect(airbrake.flush).toHaveBeenCalled()
|
|
126
|
+
process.on.mockRestore()
|
|
127
|
+
process.exit.mockRestore()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it.each([
|
|
132
|
+
['SIGINT', 130],
|
|
133
|
+
['SIGTERM', 137]
|
|
134
|
+
])('calls process.exit on %s signal with %i code', (signal, code) => {
|
|
135
|
+
jest.isolateModules(() => {
|
|
136
|
+
const signalCallbacks = {}
|
|
137
|
+
jest.spyOn(process, 'on')
|
|
138
|
+
jest.spyOn(process, 'exit')
|
|
139
|
+
process.on.mockImplementation((signalToken, callback) => {
|
|
140
|
+
signalCallbacks[signalToken] = callback
|
|
141
|
+
})
|
|
142
|
+
process.exit.mockImplementation(() => {
|
|
143
|
+
// so we don't crash out of the tests!
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
require('../recurring-payments-job.js')
|
|
147
|
+
signalCallbacks[signal]()
|
|
148
|
+
|
|
149
|
+
expect(process.exit).toHaveBeenCalledWith(code)
|
|
150
|
+
process.on.mockRestore()
|
|
151
|
+
process.exit.mockRestore()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
76
155
|
it('debug log displays "Recurring Payments job disabled" when env is false', async () => {
|
|
77
156
|
process.env.RUN_RECURRING_PAYMENTS = 'false'
|
|
78
157
|
|
|
79
|
-
await
|
|
158
|
+
await execute()
|
|
80
159
|
|
|
81
160
|
expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job disabled')
|
|
82
161
|
})
|
|
83
162
|
|
|
84
163
|
it('debug log displays "Recurring Payments job enabled" when env is true', async () => {
|
|
85
|
-
await
|
|
164
|
+
await execute()
|
|
86
165
|
|
|
87
166
|
expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job enabled')
|
|
88
167
|
})
|
|
89
168
|
|
|
90
|
-
it('
|
|
169
|
+
it('logs console error if Gov.UK Pay is not healthy', async () => {
|
|
170
|
+
jest.spyOn(console, 'error')
|
|
91
171
|
isGovPayUp.mockResolvedValueOnce(false)
|
|
92
|
-
await
|
|
172
|
+
await execute()
|
|
173
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
message: 'Run aborted, Gov.UK Pay health endpoint is reporting problems.'
|
|
176
|
+
})
|
|
177
|
+
)
|
|
178
|
+
console.error.mockReset()
|
|
93
179
|
})
|
|
94
180
|
|
|
95
181
|
it('get recurring payments is called when env is true', async () => {
|
|
96
182
|
const date = new Date().toISOString().split('T')[0]
|
|
97
183
|
|
|
98
|
-
await
|
|
184
|
+
await execute()
|
|
99
185
|
|
|
100
186
|
expect(salesApi.getDueRecurringPayments).toHaveBeenCalledWith(date)
|
|
101
187
|
})
|
|
102
188
|
|
|
103
189
|
it('debug log displays "Recurring Payments found:" when env is true', async () => {
|
|
104
|
-
await
|
|
190
|
+
await execute()
|
|
105
191
|
|
|
106
192
|
expect(debugLogger).toHaveBeenNthCalledWith(2, 'Recurring Payments found:', [])
|
|
107
193
|
})
|
|
108
194
|
|
|
109
195
|
describe('When RP fetch throws an error...', () => {
|
|
110
|
-
it('processRecurringPayments re-throws the error', async () => {
|
|
111
|
-
const error = new Error('Test error')
|
|
112
|
-
salesApi.getDueRecurringPayments.mockImplementationOnce(() => {
|
|
113
|
-
throw error
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
await expect(processRecurringPayments()).rejects.toThrowError(error)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
196
|
it('calls console.error with error message', async () => {
|
|
120
197
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
121
198
|
const error = new Error('Test error')
|
|
@@ -124,7 +201,7 @@ describe('recurring-payments-processor', () => {
|
|
|
124
201
|
})
|
|
125
202
|
|
|
126
203
|
try {
|
|
127
|
-
await
|
|
204
|
+
await execute()
|
|
128
205
|
} catch {}
|
|
129
206
|
|
|
130
207
|
expect(errorSpy).toHaveBeenCalledWith('Run aborted. Error fetching due recurring payments:', error)
|
|
@@ -138,7 +215,7 @@ describe('recurring-payments-processor', () => {
|
|
|
138
215
|
sendPayment.mockRejectedValueOnce(oopsie)
|
|
139
216
|
|
|
140
217
|
try {
|
|
141
|
-
await
|
|
218
|
+
await execute()
|
|
142
219
|
} catch {}
|
|
143
220
|
|
|
144
221
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), oopsie)
|
|
@@ -178,7 +255,7 @@ describe('recurring-payments-processor', () => {
|
|
|
178
255
|
authorisation_mode: 'agreement'
|
|
179
256
|
}
|
|
180
257
|
|
|
181
|
-
await
|
|
258
|
+
await execute()
|
|
182
259
|
|
|
183
260
|
expect(sendPayment).toHaveBeenCalledTimes(4)
|
|
184
261
|
expect(sendPayment).toHaveBeenNthCalledWith(
|
|
@@ -214,7 +291,7 @@ describe('recurring-payments-processor', () => {
|
|
|
214
291
|
salesApi.createTransaction.mockRejectedValueOnce(errors[1]).mockReturnValueOnce({ cost: 50, id: 'transaction-id-3' })
|
|
215
292
|
sendPayment.mockRejectedValueOnce(errors[2])
|
|
216
293
|
|
|
217
|
-
await
|
|
294
|
+
await execute()
|
|
218
295
|
|
|
219
296
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), ...errors)
|
|
220
297
|
})
|
|
@@ -233,7 +310,7 @@ describe('recurring-payments-processor', () => {
|
|
|
233
310
|
}
|
|
234
311
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(dueRecurringPayments)
|
|
235
312
|
|
|
236
|
-
await
|
|
313
|
+
await execute()
|
|
237
314
|
|
|
238
315
|
expect(getPaymentStatus).toHaveBeenCalledTimes(6)
|
|
239
316
|
})
|
|
@@ -246,7 +323,7 @@ describe('recurring-payments-processor', () => {
|
|
|
246
323
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
247
324
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
248
325
|
|
|
249
|
-
await
|
|
326
|
+
await execute()
|
|
250
327
|
|
|
251
328
|
expect(salesApi.preparePermissionDataForRenewal).toHaveBeenCalledWith(referenceNumber)
|
|
252
329
|
})
|
|
@@ -303,11 +380,36 @@ describe('recurring-payments-processor', () => {
|
|
|
303
380
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
304
381
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
305
382
|
|
|
306
|
-
await
|
|
383
|
+
await execute()
|
|
307
384
|
|
|
308
385
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(expectedData)
|
|
309
386
|
})
|
|
310
387
|
|
|
388
|
+
it('creates a payment journal entry', async () => {
|
|
389
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
390
|
+
const samplePayment = {
|
|
391
|
+
payment_id: Symbol('payment-id'),
|
|
392
|
+
created_date: Symbol('created-date')
|
|
393
|
+
}
|
|
394
|
+
const sampleTransaction = {
|
|
395
|
+
id: Symbol('transaction-id'),
|
|
396
|
+
cost: 99
|
|
397
|
+
}
|
|
398
|
+
sendPayment.mockResolvedValueOnce(samplePayment)
|
|
399
|
+
salesApi.createTransaction.mockResolvedValueOnce(sampleTransaction)
|
|
400
|
+
|
|
401
|
+
await execute()
|
|
402
|
+
|
|
403
|
+
expect(salesApi.createPaymentJournal).toHaveBeenCalledWith(
|
|
404
|
+
sampleTransaction.id,
|
|
405
|
+
expect.objectContaining({
|
|
406
|
+
paymentReference: samplePayment.payment_id,
|
|
407
|
+
paymentTimestamp: samplePayment.created_date,
|
|
408
|
+
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
|
|
409
|
+
})
|
|
410
|
+
)
|
|
411
|
+
})
|
|
412
|
+
|
|
311
413
|
it('strips the concession name returned by preparePermissionDataForRenewal before passing to createTransaction', async () => {
|
|
312
414
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
313
415
|
|
|
@@ -328,7 +430,7 @@ describe('recurring-payments-processor', () => {
|
|
|
328
430
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
329
431
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
330
432
|
|
|
331
|
-
await
|
|
433
|
+
await execute()
|
|
332
434
|
|
|
333
435
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
334
436
|
expect.objectContaining({
|
|
@@ -358,7 +460,7 @@ describe('recurring-payments-processor', () => {
|
|
|
358
460
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
359
461
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
360
462
|
|
|
361
|
-
await
|
|
463
|
+
await execute()
|
|
362
464
|
|
|
363
465
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
364
466
|
expect.objectContaining({
|
|
@@ -379,7 +481,7 @@ describe('recurring-payments-processor', () => {
|
|
|
379
481
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
380
482
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
381
483
|
|
|
382
|
-
await
|
|
484
|
+
await execute()
|
|
383
485
|
|
|
384
486
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
385
487
|
expect.objectContaining({
|
|
@@ -415,7 +517,7 @@ describe('recurring-payments-processor', () => {
|
|
|
415
517
|
agreement_id: agreementId
|
|
416
518
|
}
|
|
417
519
|
|
|
418
|
-
await
|
|
520
|
+
await execute()
|
|
419
521
|
|
|
420
522
|
expect(sendPayment).toHaveBeenCalledWith(expectedData)
|
|
421
523
|
})
|
|
@@ -441,7 +543,7 @@ describe('recurring-payments-processor', () => {
|
|
|
441
543
|
const mockPaymentResponse = { payment_id: 'test-payment-id', agreementId: 'agreement-1' }
|
|
442
544
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
443
545
|
|
|
444
|
-
await
|
|
546
|
+
await execute()
|
|
445
547
|
|
|
446
548
|
expect(getPaymentStatus).toHaveBeenCalledWith('test-payment-id')
|
|
447
549
|
})
|
|
@@ -468,7 +570,7 @@ describe('recurring-payments-processor', () => {
|
|
|
468
570
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
469
571
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
470
572
|
|
|
471
|
-
await
|
|
573
|
+
await execute()
|
|
472
574
|
|
|
473
575
|
console.log(debugLogger.mock.calls)
|
|
474
576
|
expect(debugLogger).toHaveBeenCalledWith(`Payment status for ${mockPaymentId}: ${PAYMENT_STATUS.Success}`)
|
|
@@ -481,7 +583,7 @@ describe('recurring-payments-processor', () => {
|
|
|
481
583
|
throw error
|
|
482
584
|
})
|
|
483
585
|
|
|
484
|
-
await
|
|
586
|
+
await execute()
|
|
485
587
|
|
|
486
588
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), error)
|
|
487
589
|
})
|
|
@@ -509,7 +611,7 @@ describe('recurring-payments-processor', () => {
|
|
|
509
611
|
const apiError = { response: { status: statusCode, data: 'boom' } }
|
|
510
612
|
getPaymentStatus.mockRejectedValueOnce(apiError)
|
|
511
613
|
|
|
512
|
-
await
|
|
614
|
+
await execute()
|
|
513
615
|
|
|
514
616
|
expect(debugLogger).toHaveBeenCalledWith(expectedMessage)
|
|
515
617
|
})
|
|
@@ -527,7 +629,7 @@ describe('recurring-payments-processor', () => {
|
|
|
527
629
|
const networkError = new Error('network meltdown')
|
|
528
630
|
getPaymentStatus.mockRejectedValueOnce(networkError)
|
|
529
631
|
|
|
530
|
-
await
|
|
632
|
+
await execute()
|
|
531
633
|
|
|
532
634
|
expect(debugLogger).toHaveBeenCalledWith(`Unexpected error fetching payment status for ${mockPaymentId}.`)
|
|
533
635
|
})
|
|
@@ -541,7 +643,7 @@ describe('recurring-payments-processor', () => {
|
|
|
541
643
|
|
|
542
644
|
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
|
|
543
645
|
|
|
544
|
-
await
|
|
646
|
+
await execute()
|
|
545
647
|
|
|
546
648
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), PAYMENT_STATUS_DELAY)
|
|
547
649
|
})
|
|
@@ -551,7 +653,7 @@ describe('recurring-payments-processor', () => {
|
|
|
551
653
|
|
|
552
654
|
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
|
|
553
655
|
|
|
554
|
-
await
|
|
656
|
+
await execute()
|
|
555
657
|
|
|
556
658
|
expect(setTimeoutSpy).not.toHaveBeenCalled()
|
|
557
659
|
})
|
|
@@ -568,7 +670,7 @@ describe('recurring-payments-processor', () => {
|
|
|
568
670
|
sendPayment.mockResolvedValueOnce({ payment_id: mockPaymentId, agreementId: 'agreement-1', created_date: mockPaymentCreatedDate })
|
|
569
671
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
570
672
|
|
|
571
|
-
await
|
|
673
|
+
await execute()
|
|
572
674
|
|
|
573
675
|
console.log(salesApi.processRPResult.mock.calls, mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
|
|
574
676
|
expect(salesApi.processRPResult).toHaveBeenCalledWith(mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
|
|
@@ -581,7 +683,7 @@ describe('recurring-payments-processor', () => {
|
|
|
581
683
|
sendPayment.mockResolvedValueOnce({ payment_id: mockPaymentId, agreementId: 'agreement-1' })
|
|
582
684
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
583
685
|
|
|
584
|
-
await
|
|
686
|
+
await execute()
|
|
585
687
|
|
|
586
688
|
expect(salesApi.processRPResult).not.toHaveBeenCalled()
|
|
587
689
|
})
|
|
@@ -602,7 +704,7 @@ describe('recurring-payments-processor', () => {
|
|
|
602
704
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
603
705
|
getPaymentStatus.mockResolvedValueOnce(mockStatus)
|
|
604
706
|
|
|
605
|
-
await
|
|
707
|
+
await execute()
|
|
606
708
|
|
|
607
709
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
608
710
|
`Payment failed. Recurring payment agreement for: ${agreementId} set to be cancelled. Updating payment journal.`
|
|
@@ -610,6 +712,30 @@ describe('recurring-payments-processor', () => {
|
|
|
610
712
|
}
|
|
611
713
|
)
|
|
612
714
|
|
|
715
|
+
it.each([
|
|
716
|
+
['a failure', 'agreement-id', getPaymentStatusFailure()],
|
|
717
|
+
['a failure', 'test-agreement-id', getPaymentStatusFailure()],
|
|
718
|
+
['a failure', 'another-agreement-id', getPaymentStatusFailure()],
|
|
719
|
+
['an error', 'agreement-id', getPaymentStatusError()],
|
|
720
|
+
['an error', 'test-agreement-id', getPaymentStatusError()],
|
|
721
|
+
['an error', 'another-agreement-id', getPaymentStatusError()]
|
|
722
|
+
])('cancelRecurringPayment is called when payment is %s', async (_status, agreementId, mockStatus) => {
|
|
723
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId })])
|
|
724
|
+
const id = Symbol('recurring-payment-id')
|
|
725
|
+
salesApi.createTransaction.mockResolvedValueOnce({
|
|
726
|
+
recurringPayment: {
|
|
727
|
+
id
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
|
|
731
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
732
|
+
getPaymentStatus.mockResolvedValueOnce(mockStatus)
|
|
733
|
+
|
|
734
|
+
await execute()
|
|
735
|
+
|
|
736
|
+
expect(salesApi.cancelRecurringPayment).toHaveBeenCalledWith(id)
|
|
737
|
+
})
|
|
738
|
+
|
|
613
739
|
it('updatePaymentJournal is called with transaction id and failed status code payment is not succesful and payment journal exists', async () => {
|
|
614
740
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
615
741
|
const transactionId = 'test-transaction-id'
|
|
@@ -622,7 +748,7 @@ describe('recurring-payments-processor', () => {
|
|
|
622
748
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
623
749
|
salesApi.getPaymentJournal.mockResolvedValueOnce(true)
|
|
624
750
|
|
|
625
|
-
await
|
|
751
|
+
await execute()
|
|
626
752
|
|
|
627
753
|
expect(salesApi.updatePaymentJournal).toHaveBeenCalledWith(transactionId, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed })
|
|
628
754
|
})
|
|
@@ -639,7 +765,7 @@ describe('recurring-payments-processor', () => {
|
|
|
639
765
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
640
766
|
salesApi.getPaymentJournal.mockResolvedValueOnce(undefined)
|
|
641
767
|
|
|
642
|
-
await
|
|
768
|
+
await execute()
|
|
643
769
|
|
|
644
770
|
expect(salesApi.updatePaymentJournal).not.toHaveBeenCalled()
|
|
645
771
|
})
|
|
@@ -665,7 +791,7 @@ describe('recurring-payments-processor', () => {
|
|
|
665
791
|
expectedData.push([reference])
|
|
666
792
|
})
|
|
667
793
|
|
|
668
|
-
await
|
|
794
|
+
await execute()
|
|
669
795
|
|
|
670
796
|
expect(salesApi.preparePermissionDataForRenewal.mock.calls).toEqual(expectedData)
|
|
671
797
|
})
|
|
@@ -709,7 +835,7 @@ describe('recurring-payments-processor', () => {
|
|
|
709
835
|
])
|
|
710
836
|
})
|
|
711
837
|
|
|
712
|
-
await
|
|
838
|
+
await execute()
|
|
713
839
|
|
|
714
840
|
expect(salesApi.createTransaction.mock.calls).toEqual(expectedData)
|
|
715
841
|
})
|
|
@@ -753,7 +879,7 @@ describe('recurring-payments-processor', () => {
|
|
|
753
879
|
])
|
|
754
880
|
})
|
|
755
881
|
|
|
756
|
-
await
|
|
882
|
+
await execute()
|
|
757
883
|
expect(sendPayment.mock.calls).toEqual(expectedData)
|
|
758
884
|
})
|
|
759
885
|
|
|
@@ -791,7 +917,7 @@ describe('recurring-payments-processor', () => {
|
|
|
791
917
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
792
918
|
})
|
|
793
919
|
|
|
794
|
-
await
|
|
920
|
+
await execute()
|
|
795
921
|
expectedData.forEach(paymentId => {
|
|
796
922
|
expect(getPaymentStatus).toHaveBeenCalledWith(paymentId)
|
|
797
923
|
})
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
import recurringPaymentsJob from 'commander'
|
|
3
|
-
import {
|
|
3
|
+
import { execute } from './recurring-payments-processor.js'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
const pkgPath = path.join(process.cwd(), 'package.json')
|
|
7
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
8
|
+
|
|
9
|
+
console.log('Recurring payments job starting at %s. name: %s. version: %s', new Date().toISOString(), pkg.name, pkg.version)
|
|
4
10
|
|
|
5
11
|
const delay = parseInt(process.env.RECURRING_PAYMENTS_LOCAL_DELAY || '0', 10)
|
|
6
12
|
if (delay > 0) {
|
|
@@ -12,7 +18,7 @@ if (delay > 0) {
|
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
function executeRecurringPaymentsJob () {
|
|
15
|
-
recurringPaymentsJob.action(
|
|
21
|
+
recurringPaymentsJob.action(execute())
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export default recurringPaymentsJob
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import moment from 'moment-timezone'
|
|
2
2
|
import { PAYMENT_STATUS, SERVICE_LOCAL_TIME, PAYMENT_JOURNAL_STATUS_CODES } from '@defra-fish/business-rules-lib'
|
|
3
|
-
import { salesApi } from '@defra-fish/connectors-lib'
|
|
3
|
+
import { salesApi, airbrake } from '@defra-fish/connectors-lib'
|
|
4
4
|
import { getPaymentStatus, sendPayment, isGovPayUp } from './services/govuk-pay-service.js'
|
|
5
5
|
import db from 'debug'
|
|
6
6
|
|
|
7
7
|
const debug = db('recurring-payments:processor')
|
|
8
8
|
|
|
9
|
+
const SIGINT_CODE = 130
|
|
10
|
+
const SIGTERM_CODE = 137
|
|
9
11
|
const PAYMENT_STATUS_DELAY = 60000
|
|
10
12
|
const MIN_CLIENT_ERROR = 400
|
|
11
13
|
const MAX_CLIENT_ERROR = 499
|
|
@@ -15,18 +17,18 @@ const MAX_SERVER_ERROR = 599
|
|
|
15
17
|
const isClientError = code => code >= MIN_CLIENT_ERROR && code <= MAX_CLIENT_ERROR
|
|
16
18
|
const isServerError = code => code >= MIN_SERVER_ERROR && code <= MAX_SERVER_ERROR
|
|
17
19
|
|
|
18
|
-
const
|
|
20
|
+
export const execute = async () => {
|
|
21
|
+
airbrake.initialise()
|
|
19
22
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
throw error
|
|
23
|
+
await processRecurringPayments()
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(e)
|
|
26
|
+
} finally {
|
|
27
|
+
await airbrake.flush()
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
const processRecurringPayments = async () => {
|
|
30
32
|
if (process.env.RUN_RECURRING_PAYMENTS?.toLowerCase() !== 'true') {
|
|
31
33
|
debug('Recurring Payments job disabled')
|
|
32
34
|
return
|
|
@@ -52,6 +54,17 @@ export const processRecurringPayments = async () => {
|
|
|
52
54
|
await Promise.allSettled(payments.map(p => processRecurringPaymentStatus(p)))
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
const fetchDueRecurringPayments = async date => {
|
|
58
|
+
try {
|
|
59
|
+
const duePayments = await salesApi.getDueRecurringPayments(date)
|
|
60
|
+
debug('Recurring Payments found:', duePayments)
|
|
61
|
+
return duePayments
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Run aborted. Error fetching due recurring payments:', error)
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
const requestPayments = async dueRCPayments => {
|
|
56
69
|
const paymentRequestResults = await Promise.allSettled(dueRCPayments.map(processRecurringPayment))
|
|
57
70
|
const payments = paymentRequestResults.filter(prr => prr.status === 'fulfilled').map(p => p.value)
|
|
@@ -77,6 +90,12 @@ const createNewTransaction = async (referenceNumber, recurringPayment) => {
|
|
|
77
90
|
const takeRecurringPayment = async (agreementId, transaction) => {
|
|
78
91
|
const preparedPayment = preparePayment(agreementId, transaction)
|
|
79
92
|
const payment = await sendPayment(preparedPayment)
|
|
93
|
+
await salesApi.createPaymentJournal(transaction.id, {
|
|
94
|
+
paymentReference: payment.payment_id,
|
|
95
|
+
paymentTimestamp: payment.created_date,
|
|
96
|
+
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
|
|
97
|
+
})
|
|
98
|
+
|
|
80
99
|
return {
|
|
81
100
|
agreementId,
|
|
82
101
|
paymentId: payment.payment_id,
|
|
@@ -148,6 +167,7 @@ const processRecurringPaymentStatus = async payment => {
|
|
|
148
167
|
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed
|
|
149
168
|
})
|
|
150
169
|
}
|
|
170
|
+
await salesApi.cancelRecurringPayment(payment.transaction.recurringPayment.id)
|
|
151
171
|
}
|
|
152
172
|
} catch (error) {
|
|
153
173
|
const status = error.response?.status
|
|
@@ -161,3 +181,11 @@ const processRecurringPaymentStatus = async payment => {
|
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
}
|
|
184
|
+
|
|
185
|
+
const shutdown = code => {
|
|
186
|
+
airbrake.flush()
|
|
187
|
+
process.exit(code)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.on('SIGINT', () => shutdown(SIGINT_CODE))
|
|
191
|
+
process.on('SIGTERM', () => shutdown(SIGTERM_CODE))
|