@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-rc.8",
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.8",
40
- "@defra-fish/connectors-lib": "1.63.0-rc.8",
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": "4376fb63f6e1867708241521e43c4bce5791e08a"
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,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
- getDueRecurringPayments: jest.fn(() => []),
24
- preparePermissionDataForRenewal: jest.fn(() => ({
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 processRecurringPayments()
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 processRecurringPayments()
164
+ await execute()
86
165
 
87
166
  expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job enabled')
88
167
  })
89
168
 
90
- 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')
91
171
  isGovPayUp.mockResolvedValueOnce(false)
92
- 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()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 processRecurringPayments()
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 { 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)
@@ -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))