@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-rc.9",
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-rc.9",
40
- "@defra-fish/connectors-lib": "1.63.0-rc.9",
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": "fc11192e1947a9a254138b543c8718ad892f0a15"
45
+ "gitHead": "7cfb8ef668002fc340b755dd3aaa4572063e115c"
46
46
  }
@@ -1,49 +1,66 @@
1
1
  import commander from 'commander'
2
- import { processRecurringPayments } from '../recurring-payments-processor.js'
2
+ import { execute } from '../recurring-payments-processor.js'
3
+ import fs from 'fs'
3
4
 
4
5
  jest.useFakeTimers()
5
-
6
- jest.mock('../recurring-payments-processor.js', () => {
7
- if (!global.processRecurringPayments) {
8
- global.processRecurringPayments = jest.fn()
6
+ jest.mock('../recurring-payments-processor.js')
7
+ jest.mock('fs', () => ({
8
+ readFileSync: jest.fn(),
9
+ promises: {
10
+ readFile: jest.fn()
9
11
  }
10
- return { processRecurringPayments: global.processRecurringPayments }
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('calls processRecurringPayments when no delay', () => {
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(processRecurringPayments).toHaveBeenCalled()
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 processRecurringPayments when delay', () => {
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(processRecurringPayments).toHaveBeenCalled()
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 { processRecurringPayments } from '../recurring-payments-processor.js'
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 processRecurringPayments()
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 processRecurringPayments()
164
+ await execute()
87
165
 
88
166
  expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job enabled')
89
167
  })
90
168
 
91
- it('throws if Gov.UK Pay is not healthy', async () => {
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 expect(() => processRecurringPayments()).rejects.toThrow('Run aborted, Gov.UK Pay health endpoint is reporting problems.')
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 { processRecurringPayments } from './recurring-payments-processor.js'
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(processRecurringPayments())
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 fetchDueRecurringPayments = async date => {
20
+ export const execute = async () => {
21
+ airbrake.initialise()
19
22
  try {
20
- const duePayments = await salesApi.getDueRecurringPayments(date)
21
- debug('Recurring Payments found:', duePayments)
22
- return duePayments
23
- } catch (error) {
24
- console.error('Run aborted. Error fetching due recurring payments:', error)
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
- export const processRecurringPayments = async () => {
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))