@defra-fish/recurring-payments-job 1.63.0-rc.9 → 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,7 +19,12 @@ 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: {
|
|
27
|
+
cancelRecurringPayment: jest.fn(),
|
|
23
28
|
createPaymentJournal: jest.fn(),
|
|
24
29
|
createTransaction: jest.fn(() => ({
|
|
25
30
|
id: 'test-transaction-id',
|
|
@@ -74,49 +79,120 @@ describe('recurring-payments-processor', () => {
|
|
|
74
79
|
global.setTimeout = jest.fn((cb, ms) => cb())
|
|
75
80
|
})
|
|
76
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
|
+
|
|
77
155
|
it('debug log displays "Recurring Payments job disabled" when env is false', async () => {
|
|
78
156
|
process.env.RUN_RECURRING_PAYMENTS = 'false'
|
|
79
157
|
|
|
80
|
-
await
|
|
158
|
+
await execute()
|
|
81
159
|
|
|
82
160
|
expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job disabled')
|
|
83
161
|
})
|
|
84
162
|
|
|
85
163
|
it('debug log displays "Recurring Payments job enabled" when env is true', async () => {
|
|
86
|
-
await
|
|
164
|
+
await execute()
|
|
87
165
|
|
|
88
166
|
expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job enabled')
|
|
89
167
|
})
|
|
90
168
|
|
|
91
|
-
it('
|
|
169
|
+
it('logs console error if Gov.UK Pay is not healthy', async () => {
|
|
170
|
+
jest.spyOn(console, 'error')
|
|
92
171
|
isGovPayUp.mockResolvedValueOnce(false)
|
|
93
|
-
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()
|
|
94
179
|
})
|
|
95
180
|
|
|
96
181
|
it('get recurring payments is called when env is true', async () => {
|
|
97
182
|
const date = new Date().toISOString().split('T')[0]
|
|
98
183
|
|
|
99
|
-
await
|
|
184
|
+
await execute()
|
|
100
185
|
|
|
101
186
|
expect(salesApi.getDueRecurringPayments).toHaveBeenCalledWith(date)
|
|
102
187
|
})
|
|
103
188
|
|
|
104
189
|
it('debug log displays "Recurring Payments found:" when env is true', async () => {
|
|
105
|
-
await
|
|
190
|
+
await execute()
|
|
106
191
|
|
|
107
192
|
expect(debugLogger).toHaveBeenNthCalledWith(2, 'Recurring Payments found:', [])
|
|
108
193
|
})
|
|
109
194
|
|
|
110
195
|
describe('When RP fetch throws an error...', () => {
|
|
111
|
-
it('processRecurringPayments re-throws the error', async () => {
|
|
112
|
-
const error = new Error('Test error')
|
|
113
|
-
salesApi.getDueRecurringPayments.mockImplementationOnce(() => {
|
|
114
|
-
throw error
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
await expect(processRecurringPayments()).rejects.toThrowError(error)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
196
|
it('calls console.error with error message', async () => {
|
|
121
197
|
const errorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
122
198
|
const error = new Error('Test error')
|
|
@@ -125,7 +201,7 @@ describe('recurring-payments-processor', () => {
|
|
|
125
201
|
})
|
|
126
202
|
|
|
127
203
|
try {
|
|
128
|
-
await
|
|
204
|
+
await execute()
|
|
129
205
|
} catch {}
|
|
130
206
|
|
|
131
207
|
expect(errorSpy).toHaveBeenCalledWith('Run aborted. Error fetching due recurring payments:', error)
|
|
@@ -139,7 +215,7 @@ describe('recurring-payments-processor', () => {
|
|
|
139
215
|
sendPayment.mockRejectedValueOnce(oopsie)
|
|
140
216
|
|
|
141
217
|
try {
|
|
142
|
-
await
|
|
218
|
+
await execute()
|
|
143
219
|
} catch {}
|
|
144
220
|
|
|
145
221
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), oopsie)
|
|
@@ -179,7 +255,7 @@ describe('recurring-payments-processor', () => {
|
|
|
179
255
|
authorisation_mode: 'agreement'
|
|
180
256
|
}
|
|
181
257
|
|
|
182
|
-
await
|
|
258
|
+
await execute()
|
|
183
259
|
|
|
184
260
|
expect(sendPayment).toHaveBeenCalledTimes(4)
|
|
185
261
|
expect(sendPayment).toHaveBeenNthCalledWith(
|
|
@@ -215,7 +291,7 @@ describe('recurring-payments-processor', () => {
|
|
|
215
291
|
salesApi.createTransaction.mockRejectedValueOnce(errors[1]).mockReturnValueOnce({ cost: 50, id: 'transaction-id-3' })
|
|
216
292
|
sendPayment.mockRejectedValueOnce(errors[2])
|
|
217
293
|
|
|
218
|
-
await
|
|
294
|
+
await execute()
|
|
219
295
|
|
|
220
296
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), ...errors)
|
|
221
297
|
})
|
|
@@ -234,7 +310,7 @@ describe('recurring-payments-processor', () => {
|
|
|
234
310
|
}
|
|
235
311
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(dueRecurringPayments)
|
|
236
312
|
|
|
237
|
-
await
|
|
313
|
+
await execute()
|
|
238
314
|
|
|
239
315
|
expect(getPaymentStatus).toHaveBeenCalledTimes(6)
|
|
240
316
|
})
|
|
@@ -247,7 +323,7 @@ describe('recurring-payments-processor', () => {
|
|
|
247
323
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
248
324
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
249
325
|
|
|
250
|
-
await
|
|
326
|
+
await execute()
|
|
251
327
|
|
|
252
328
|
expect(salesApi.preparePermissionDataForRenewal).toHaveBeenCalledWith(referenceNumber)
|
|
253
329
|
})
|
|
@@ -304,7 +380,7 @@ describe('recurring-payments-processor', () => {
|
|
|
304
380
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
305
381
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
306
382
|
|
|
307
|
-
await
|
|
383
|
+
await execute()
|
|
308
384
|
|
|
309
385
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(expectedData)
|
|
310
386
|
})
|
|
@@ -322,7 +398,7 @@ describe('recurring-payments-processor', () => {
|
|
|
322
398
|
sendPayment.mockResolvedValueOnce(samplePayment)
|
|
323
399
|
salesApi.createTransaction.mockResolvedValueOnce(sampleTransaction)
|
|
324
400
|
|
|
325
|
-
await
|
|
401
|
+
await execute()
|
|
326
402
|
|
|
327
403
|
expect(salesApi.createPaymentJournal).toHaveBeenCalledWith(
|
|
328
404
|
sampleTransaction.id,
|
|
@@ -354,7 +430,7 @@ describe('recurring-payments-processor', () => {
|
|
|
354
430
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
355
431
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
356
432
|
|
|
357
|
-
await
|
|
433
|
+
await execute()
|
|
358
434
|
|
|
359
435
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
360
436
|
expect.objectContaining({
|
|
@@ -384,7 +460,7 @@ describe('recurring-payments-processor', () => {
|
|
|
384
460
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
385
461
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
386
462
|
|
|
387
|
-
await
|
|
463
|
+
await execute()
|
|
388
464
|
|
|
389
465
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
390
466
|
expect.objectContaining({
|
|
@@ -405,7 +481,7 @@ describe('recurring-payments-processor', () => {
|
|
|
405
481
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
406
482
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
407
483
|
|
|
408
|
-
await
|
|
484
|
+
await execute()
|
|
409
485
|
|
|
410
486
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
411
487
|
expect.objectContaining({
|
|
@@ -441,7 +517,7 @@ describe('recurring-payments-processor', () => {
|
|
|
441
517
|
agreement_id: agreementId
|
|
442
518
|
}
|
|
443
519
|
|
|
444
|
-
await
|
|
520
|
+
await execute()
|
|
445
521
|
|
|
446
522
|
expect(sendPayment).toHaveBeenCalledWith(expectedData)
|
|
447
523
|
})
|
|
@@ -467,7 +543,7 @@ describe('recurring-payments-processor', () => {
|
|
|
467
543
|
const mockPaymentResponse = { payment_id: 'test-payment-id', agreementId: 'agreement-1' }
|
|
468
544
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
469
545
|
|
|
470
|
-
await
|
|
546
|
+
await execute()
|
|
471
547
|
|
|
472
548
|
expect(getPaymentStatus).toHaveBeenCalledWith('test-payment-id')
|
|
473
549
|
})
|
|
@@ -494,7 +570,7 @@ describe('recurring-payments-processor', () => {
|
|
|
494
570
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
495
571
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
496
572
|
|
|
497
|
-
await
|
|
573
|
+
await execute()
|
|
498
574
|
|
|
499
575
|
console.log(debugLogger.mock.calls)
|
|
500
576
|
expect(debugLogger).toHaveBeenCalledWith(`Payment status for ${mockPaymentId}: ${PAYMENT_STATUS.Success}`)
|
|
@@ -507,7 +583,7 @@ describe('recurring-payments-processor', () => {
|
|
|
507
583
|
throw error
|
|
508
584
|
})
|
|
509
585
|
|
|
510
|
-
await
|
|
586
|
+
await execute()
|
|
511
587
|
|
|
512
588
|
expect(debugLogger).toHaveBeenCalledWith(expect.any(String), error)
|
|
513
589
|
})
|
|
@@ -535,7 +611,7 @@ describe('recurring-payments-processor', () => {
|
|
|
535
611
|
const apiError = { response: { status: statusCode, data: 'boom' } }
|
|
536
612
|
getPaymentStatus.mockRejectedValueOnce(apiError)
|
|
537
613
|
|
|
538
|
-
await
|
|
614
|
+
await execute()
|
|
539
615
|
|
|
540
616
|
expect(debugLogger).toHaveBeenCalledWith(expectedMessage)
|
|
541
617
|
})
|
|
@@ -553,7 +629,7 @@ describe('recurring-payments-processor', () => {
|
|
|
553
629
|
const networkError = new Error('network meltdown')
|
|
554
630
|
getPaymentStatus.mockRejectedValueOnce(networkError)
|
|
555
631
|
|
|
556
|
-
await
|
|
632
|
+
await execute()
|
|
557
633
|
|
|
558
634
|
expect(debugLogger).toHaveBeenCalledWith(`Unexpected error fetching payment status for ${mockPaymentId}.`)
|
|
559
635
|
})
|
|
@@ -567,7 +643,7 @@ describe('recurring-payments-processor', () => {
|
|
|
567
643
|
|
|
568
644
|
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
|
|
569
645
|
|
|
570
|
-
await
|
|
646
|
+
await execute()
|
|
571
647
|
|
|
572
648
|
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), PAYMENT_STATUS_DELAY)
|
|
573
649
|
})
|
|
@@ -577,7 +653,7 @@ describe('recurring-payments-processor', () => {
|
|
|
577
653
|
|
|
578
654
|
const setTimeoutSpy = jest.spyOn(global, 'setTimeout').mockImplementation(cb => cb())
|
|
579
655
|
|
|
580
|
-
await
|
|
656
|
+
await execute()
|
|
581
657
|
|
|
582
658
|
expect(setTimeoutSpy).not.toHaveBeenCalled()
|
|
583
659
|
})
|
|
@@ -594,7 +670,7 @@ describe('recurring-payments-processor', () => {
|
|
|
594
670
|
sendPayment.mockResolvedValueOnce({ payment_id: mockPaymentId, agreementId: 'agreement-1', created_date: mockPaymentCreatedDate })
|
|
595
671
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
596
672
|
|
|
597
|
-
await
|
|
673
|
+
await execute()
|
|
598
674
|
|
|
599
675
|
console.log(salesApi.processRPResult.mock.calls, mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
|
|
600
676
|
expect(salesApi.processRPResult).toHaveBeenCalledWith(mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
|
|
@@ -607,7 +683,7 @@ describe('recurring-payments-processor', () => {
|
|
|
607
683
|
sendPayment.mockResolvedValueOnce({ payment_id: mockPaymentId, agreementId: 'agreement-1' })
|
|
608
684
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
609
685
|
|
|
610
|
-
await
|
|
686
|
+
await execute()
|
|
611
687
|
|
|
612
688
|
expect(salesApi.processRPResult).not.toHaveBeenCalled()
|
|
613
689
|
})
|
|
@@ -628,7 +704,7 @@ describe('recurring-payments-processor', () => {
|
|
|
628
704
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
629
705
|
getPaymentStatus.mockResolvedValueOnce(mockStatus)
|
|
630
706
|
|
|
631
|
-
await
|
|
707
|
+
await execute()
|
|
632
708
|
|
|
633
709
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
634
710
|
`Payment failed. Recurring payment agreement for: ${agreementId} set to be cancelled. Updating payment journal.`
|
|
@@ -636,6 +712,30 @@ describe('recurring-payments-processor', () => {
|
|
|
636
712
|
}
|
|
637
713
|
)
|
|
638
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
|
+
|
|
639
739
|
it('updatePaymentJournal is called with transaction id and failed status code payment is not succesful and payment journal exists', async () => {
|
|
640
740
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
641
741
|
const transactionId = 'test-transaction-id'
|
|
@@ -648,7 +748,7 @@ describe('recurring-payments-processor', () => {
|
|
|
648
748
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
649
749
|
salesApi.getPaymentJournal.mockResolvedValueOnce(true)
|
|
650
750
|
|
|
651
|
-
await
|
|
751
|
+
await execute()
|
|
652
752
|
|
|
653
753
|
expect(salesApi.updatePaymentJournal).toHaveBeenCalledWith(transactionId, { paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed })
|
|
654
754
|
})
|
|
@@ -665,7 +765,7 @@ describe('recurring-payments-processor', () => {
|
|
|
665
765
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusFailure())
|
|
666
766
|
salesApi.getPaymentJournal.mockResolvedValueOnce(undefined)
|
|
667
767
|
|
|
668
|
-
await
|
|
768
|
+
await execute()
|
|
669
769
|
|
|
670
770
|
expect(salesApi.updatePaymentJournal).not.toHaveBeenCalled()
|
|
671
771
|
})
|
|
@@ -691,7 +791,7 @@ describe('recurring-payments-processor', () => {
|
|
|
691
791
|
expectedData.push([reference])
|
|
692
792
|
})
|
|
693
793
|
|
|
694
|
-
await
|
|
794
|
+
await execute()
|
|
695
795
|
|
|
696
796
|
expect(salesApi.preparePermissionDataForRenewal.mock.calls).toEqual(expectedData)
|
|
697
797
|
})
|
|
@@ -735,7 +835,7 @@ describe('recurring-payments-processor', () => {
|
|
|
735
835
|
])
|
|
736
836
|
})
|
|
737
837
|
|
|
738
|
-
await
|
|
838
|
+
await execute()
|
|
739
839
|
|
|
740
840
|
expect(salesApi.createTransaction.mock.calls).toEqual(expectedData)
|
|
741
841
|
})
|
|
@@ -779,7 +879,7 @@ describe('recurring-payments-processor', () => {
|
|
|
779
879
|
])
|
|
780
880
|
})
|
|
781
881
|
|
|
782
|
-
await
|
|
882
|
+
await execute()
|
|
783
883
|
expect(sendPayment.mock.calls).toEqual(expectedData)
|
|
784
884
|
})
|
|
785
885
|
|
|
@@ -817,7 +917,7 @@ describe('recurring-payments-processor', () => {
|
|
|
817
917
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
818
918
|
})
|
|
819
919
|
|
|
820
|
-
await
|
|
920
|
+
await execute()
|
|
821
921
|
expectedData.forEach(paymentId => {
|
|
822
922
|
expect(getPaymentStatus).toHaveBeenCalledWith(paymentId)
|
|
823
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)
|
|
@@ -154,6 +167,7 @@ const processRecurringPaymentStatus = async payment => {
|
|
|
154
167
|
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed
|
|
155
168
|
})
|
|
156
169
|
}
|
|
170
|
+
await salesApi.cancelRecurringPayment(payment.transaction.recurringPayment.id)
|
|
157
171
|
}
|
|
158
172
|
} catch (error) {
|
|
159
173
|
const status = error.response?.status
|
|
@@ -167,3 +181,11 @@ const processRecurringPaymentStatus = async payment => {
|
|
|
167
181
|
}
|
|
168
182
|
}
|
|
169
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))
|