@defra-fish/recurring-payments-job 1.62.0-rc.8 → 1.62.0-rc.9

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.62.0-rc.8",
3
+ "version": "1.62.0-rc.9",
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.62.0-rc.8",
40
- "@defra-fish/connectors-lib": "1.62.0-rc.8",
39
+ "@defra-fish/business-rules-lib": "1.62.0-rc.9",
40
+ "@defra-fish/connectors-lib": "1.62.0-rc.9",
41
41
  "commander": "^7.2.0",
42
42
  "debug": "^4.3.3",
43
43
  "moment-timezone": "^0.5.34"
44
44
  },
45
- "gitHead": "5d9e947d9ce87f6f43aa6ca7ac8d78f797ecebdf"
45
+ "gitHead": "435e74ebb124eeda4a409f5aa256ba188125f06b"
46
46
  }
@@ -1,7 +1,7 @@
1
1
  import { salesApi } from '@defra-fish/connectors-lib'
2
- import { PAYMENT_JOURNAL_STATUS_CODES } from '@defra-fish/business-rules-lib'
2
+ import { PAYMENT_STATUS, PAYMENT_JOURNAL_STATUS_CODES } from '@defra-fish/business-rules-lib'
3
3
  import { processRecurringPayments } from '../recurring-payments-processor.js'
4
- import { getPaymentStatus, sendPayment } from '../services/govuk-pay-service.js'
4
+ import { getPaymentStatus, isGovPayUp, sendPayment } from '../services/govuk-pay-service.js'
5
5
  import db from 'debug'
6
6
 
7
7
  jest.mock('@defra-fish/business-rules-lib', () => ({
@@ -33,13 +33,15 @@ jest.mock('@defra-fish/connectors-lib', () => ({
33
33
  getPaymentJournal: jest.fn()
34
34
  }
35
35
  }))
36
+
36
37
  jest.mock('../services/govuk-pay-service.js', () => ({
37
- sendPayment: jest.fn(),
38
- getPaymentStatus: jest.fn()
38
+ sendPayment: jest.fn(() => ({ payment_id: 'payment_id', created_date: '2025-07-18T09:00:00.000Z' })),
39
+ getPaymentStatus: jest.fn(),
40
+ isGovPayUp: jest.fn(() => true)
39
41
  }))
42
+
40
43
  jest.mock('debug', () => jest.fn(() => jest.fn()))
41
44
 
42
- const debugMock = db.mock.results[0].value
43
45
  const PAYMENT_STATUS_DELAY = 60000
44
46
  const getPaymentStatusSuccess = () => ({ state: { status: 'payment status success' } })
45
47
  const getPaymentStatusFailure = () => ({ state: { status: 'payment status failure' } })
@@ -57,25 +59,37 @@ const getMockPaymentRequestResponse = () => [
57
59
  }
58
60
  ]
59
61
 
62
+ const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = 'test-agreement-id') => ({
63
+ entity: { agreementId },
64
+ expanded: { activePermission: { entity: { referenceNumber } } }
65
+ })
66
+
60
67
  describe('recurring-payments-processor', () => {
68
+ const [{ value: debugLogger }] = db.mock.results
69
+
61
70
  beforeEach(() => {
62
71
  jest.clearAllMocks()
63
72
  process.env.RUN_RECURRING_PAYMENTS = 'true'
64
73
  global.setTimeout = jest.fn((cb, ms) => cb())
65
74
  })
66
75
 
67
- it('debug displays "Recurring Payments job disabled" when env is false', async () => {
76
+ it('debug log displays "Recurring Payments job disabled" when env is false', async () => {
68
77
  process.env.RUN_RECURRING_PAYMENTS = 'false'
69
78
 
70
79
  await processRecurringPayments()
71
80
 
72
- expect(debugMock).toHaveBeenCalledWith('Recurring Payments job disabled')
81
+ expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job disabled')
73
82
  })
74
83
 
75
- it('debug displays "Recurring Payments job enabled" when env is true', async () => {
84
+ it('debug log displays "Recurring Payments job enabled" when env is true', async () => {
76
85
  await processRecurringPayments()
77
86
 
78
- expect(debugMock).toHaveBeenCalledWith('Recurring Payments job enabled')
87
+ expect(debugLogger).toHaveBeenCalledWith('Recurring Payments job enabled')
88
+ })
89
+
90
+ it('throws if Gov.UK Pay is not healthy', async () => {
91
+ isGovPayUp.mockResolvedValueOnce(false)
92
+ await expect(() => processRecurringPayments()).rejects.toThrow('Run aborted, Gov.UK Pay health endpoint is reporting problems.')
79
93
  })
80
94
 
81
95
  it('get recurring payments is called when env is true', async () => {
@@ -86,10 +100,143 @@ describe('recurring-payments-processor', () => {
86
100
  expect(salesApi.getDueRecurringPayments).toHaveBeenCalledWith(date)
87
101
  })
88
102
 
89
- it('debug displays "Recurring Payments found: " when env is true', async () => {
103
+ it('debug log displays "Recurring Payments found:" when env is true', async () => {
90
104
  await processRecurringPayments()
91
105
 
92
- expect(debugMock).toHaveBeenCalledWith('Recurring Payments found: ', [])
106
+ expect(debugLogger).toHaveBeenNthCalledWith(2, 'Recurring Payments found:', [])
107
+ })
108
+
109
+ 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
+ it('calls console.error with error message', async () => {
120
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
121
+ const error = new Error('Test error')
122
+ salesApi.getDueRecurringPayments.mockImplementationOnce(() => {
123
+ throw error
124
+ })
125
+
126
+ try {
127
+ await processRecurringPayments()
128
+ } catch {}
129
+
130
+ expect(errorSpy).toHaveBeenCalledWith('Run aborted. Error fetching due recurring payments:', error)
131
+ })
132
+ })
133
+
134
+ describe('When payment request throws an error...', () => {
135
+ it('debug is called with error message', async () => {
136
+ salesApi.getDueRecurringPayments.mockReturnValueOnce(getMockPaymentRequestResponse())
137
+ const oopsie = new Error('payment gate down')
138
+ sendPayment.mockRejectedValueOnce(oopsie)
139
+
140
+ try {
141
+ await processRecurringPayments()
142
+ } catch {}
143
+
144
+ expect(debugLogger).toHaveBeenCalledWith(expect.any(String), oopsie)
145
+ })
146
+
147
+ it('prepares and sends all payment requests, even if some fail', async () => {
148
+ const agreementIds = [Symbol('agreementId1'), Symbol('agreementId2'), Symbol('agreementId3'), Symbol('agreementId4')]
149
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([
150
+ getMockDueRecurringPayment('fee', agreementIds[0]),
151
+ getMockDueRecurringPayment('fi', agreementIds[1]),
152
+ getMockDueRecurringPayment('foe', agreementIds[2]),
153
+ getMockDueRecurringPayment('fum', agreementIds[3])
154
+ ])
155
+
156
+ const permissionData = { licensee: { countryCode: 'GB-ENG' } }
157
+ for (let x = 0; x < agreementIds.length; x++) {
158
+ salesApi.preparePermissionDataForRenewal.mockReturnValueOnce(permissionData)
159
+ salesApi.createTransaction.mockReturnValueOnce({
160
+ cost: 50,
161
+ id: `transaction-id-${x + 1}`
162
+ })
163
+
164
+ if (x === 1) {
165
+ const err = new Error('Payment request failed')
166
+ sendPayment.mockRejectedValueOnce(err)
167
+ } else {
168
+ sendPayment.mockResolvedValueOnce({ payment_id: `test-payment-id-${x + 1}`, agreementId: agreementIds[x] })
169
+ }
170
+ if (x < 3) {
171
+ getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
172
+ }
173
+ }
174
+ const expectedData = {
175
+ amount: 5000,
176
+ description: 'The recurring card payment for your rod fishing licence',
177
+ reference: 'transactionId',
178
+ authorisation_mode: 'agreement'
179
+ }
180
+
181
+ await processRecurringPayments()
182
+
183
+ expect(sendPayment).toHaveBeenCalledTimes(4)
184
+ expect(sendPayment).toHaveBeenNthCalledWith(
185
+ 1,
186
+ expect.objectContaining({ ...expectedData, reference: 'transaction-id-1', agreement_id: agreementIds[0] })
187
+ )
188
+ expect(sendPayment).toHaveBeenNthCalledWith(
189
+ 2,
190
+ expect.objectContaining({ ...expectedData, reference: 'transaction-id-2', agreement_id: agreementIds[1] })
191
+ )
192
+ expect(sendPayment).toHaveBeenNthCalledWith(
193
+ 3,
194
+ expect.objectContaining({ ...expectedData, reference: 'transaction-id-3', agreement_id: agreementIds[2] })
195
+ )
196
+ expect(sendPayment).toHaveBeenNthCalledWith(
197
+ 4,
198
+ expect.objectContaining({ ...expectedData, reference: 'transaction-id-4', agreement_id: agreementIds[3] })
199
+ )
200
+ })
201
+
202
+ it('logs an error for every failure', async () => {
203
+ const errors = [new Error('error 1'), new Error('error 2'), new Error('error 3')]
204
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([
205
+ getMockDueRecurringPayment('fee', 'a1'),
206
+ getMockDueRecurringPayment('fi', 'a2'),
207
+ getMockDueRecurringPayment('foe', 'a3')
208
+ ])
209
+ const permissionData = { licensee: { countryCode: 'GB-ENG' } }
210
+ salesApi.preparePermissionDataForRenewal
211
+ .mockRejectedValueOnce(errors[0])
212
+ .mockReturnValueOnce(permissionData)
213
+ .mockReturnValueOnce(permissionData)
214
+ salesApi.createTransaction.mockRejectedValueOnce(errors[1]).mockReturnValueOnce({ cost: 50, id: 'transaction-id-3' })
215
+ sendPayment.mockRejectedValueOnce(errors[2])
216
+
217
+ await processRecurringPayments()
218
+
219
+ expect(debugLogger).toHaveBeenCalledWith(expect.any(String), ...errors)
220
+ })
221
+ })
222
+
223
+ describe('When payment status request throws an error...', () => {
224
+ it('processRecurringPayments requests payment status for all payments, even if some throw errors', async () => {
225
+ const dueRecurringPayments = []
226
+ for (let x = 0; x < 6; x++) {
227
+ dueRecurringPayments.push(getMockDueRecurringPayment())
228
+ if ([1, 3].includes(x)) {
229
+ getPaymentStatus.mockRejectedValueOnce(new Error(`status failure ${x}`))
230
+ } else {
231
+ getPaymentStatus.mockReturnValueOnce(getPaymentStatusSuccess())
232
+ }
233
+ }
234
+ salesApi.getDueRecurringPayments.mockReturnValueOnce(dueRecurringPayments)
235
+
236
+ await processRecurringPayments()
237
+
238
+ expect(getPaymentStatus).toHaveBeenCalledTimes(6)
239
+ })
93
240
  })
94
241
 
95
242
  it('prepares the data for found recurring payments', async () => {
@@ -236,16 +383,6 @@ describe('recurring-payments-processor', () => {
236
383
  )
237
384
  })
238
385
 
239
- it('raises an error if createTransaction fails', async () => {
240
- salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
241
- const error = 'Wuh-oh!'
242
- salesApi.createTransaction.mockImplementationOnce(() => {
243
- throw new Error(error)
244
- })
245
-
246
- await expect(processRecurringPayments()).rejects.toThrowError(error)
247
- })
248
-
249
386
  it('prepares and sends the payment request', async () => {
250
387
  const agreementId = Symbol('agreementId')
251
388
  const transactionId = 'transactionId'
@@ -304,7 +441,7 @@ describe('recurring-payments-processor', () => {
304
441
  expect(getPaymentStatus).toHaveBeenCalledWith('test-payment-id')
305
442
  })
306
443
 
307
- it('debug should log payment status for recurring payment', async () => {
444
+ it('should log payment status for recurring payment', async () => {
308
445
  const mockPaymentId = 'test-payment-id'
309
446
  const mockResponse = [
310
447
  {
@@ -318,48 +455,76 @@ describe('recurring-payments-processor', () => {
318
455
  }
319
456
  }
320
457
  ]
321
- const mockStatus = getPaymentStatusSuccess()
322
458
  salesApi.getDueRecurringPayments.mockResolvedValueOnce(mockResponse)
323
459
  salesApi.createTransaction.mockResolvedValueOnce({
324
460
  id: mockPaymentId
325
461
  })
326
462
  const mockPaymentResponse = { payment_id: mockPaymentId, agreementId: 'agreement-1' }
327
463
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
328
- getPaymentStatus.mockResolvedValueOnce(mockStatus)
464
+ getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
329
465
 
330
466
  await processRecurringPayments()
331
467
 
332
- expect(debugMock).toHaveBeenCalledWith(`Payment status for ${mockPaymentId}: ${mockStatus.state.status}`)
468
+ console.log(debugLogger.mock.calls)
469
+ expect(debugLogger).toHaveBeenCalledWith(`Payment status for ${mockPaymentId}: ${PAYMENT_STATUS.Success}`)
333
470
  })
334
471
 
335
- it('debug should inform that the recurring payment has been processed', async () => {
336
- const mockTransactionId = 'test-transaction-id'
337
- salesApi.createTransaction.mockReturnValueOnce({
338
- cost: 50
472
+ it('logs an error if createTransaction fails', async () => {
473
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
474
+ const error = new Error('Wuh-oh!')
475
+ salesApi.createTransaction.mockImplementationOnce(() => {
476
+ throw error
339
477
  })
478
+
479
+ await processRecurringPayments()
480
+
481
+ expect(debugLogger).toHaveBeenCalledWith(expect.any(String), error)
482
+ })
483
+
484
+ it.each([
485
+ [400, 'Failed to fetch status for payment test-payment-id, error 400'],
486
+ [486, 'Failed to fetch status for payment test-payment-id, error 486'],
487
+ [499, 'Failed to fetch status for payment test-payment-id, error 499'],
488
+ [500, 'Payment status API error for test-payment-id, error 500'],
489
+ [512, 'Payment status API error for test-payment-id, error 512'],
490
+ [599, 'Payment status API error for test-payment-id, error 599']
491
+ ])('logs the correct message when getPaymentStatus rejects with HTTP %i', async (statusCode, expectedMessage) => {
492
+ const mockPaymentId = 'test-payment-id'
340
493
  const mockResponse = [
341
- {
342
- entity: { agreementId: 'agreement-1' },
343
- expanded: {
344
- activePermission: {
345
- entity: {
346
- referenceNumber: 'ref-1'
347
- }
348
- }
349
- }
350
- }
494
+ { entity: { agreementId: 'agreement-1' }, expanded: { activePermission: { entity: { referenceNumber: 'ref-1' } } } }
351
495
  ]
352
496
  salesApi.getDueRecurringPayments.mockResolvedValueOnce(mockResponse)
353
- salesApi.createTransaction.mockResolvedValueOnce({
354
- id: mockTransactionId
497
+ salesApi.createTransaction.mockResolvedValueOnce({ id: mockPaymentId })
498
+ sendPayment.mockResolvedValueOnce({
499
+ payment_id: mockPaymentId,
500
+ agreementId: 'agreement-1',
501
+ created_date: '2025-04-30T12:00:00Z'
355
502
  })
356
- const mockPaymentResponse = { payment_id: 'payment-id', agreementId: 'agreement-1' }
357
- sendPayment.mockResolvedValueOnce(mockPaymentResponse)
358
- getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
503
+
504
+ const apiError = { response: { status: statusCode, data: 'boom' } }
505
+ getPaymentStatus.mockRejectedValueOnce(apiError)
359
506
 
360
507
  await processRecurringPayments()
361
508
 
362
- expect(debugMock).toHaveBeenCalledWith(`Processed Recurring Payment for ${mockTransactionId}`)
509
+ expect(debugLogger).toHaveBeenCalledWith(expectedMessage)
510
+ })
511
+
512
+ it('logs the generic unexpected-error message and still rejects', async () => {
513
+ const mockPaymentId = 'test-payment-id'
514
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce(getMockPaymentRequestResponse())
515
+ salesApi.createTransaction.mockResolvedValueOnce({ id: mockPaymentId })
516
+ sendPayment.mockResolvedValueOnce({
517
+ payment_id: mockPaymentId,
518
+ agreementId: 'agreement-1',
519
+ created_date: '2025-04-30T12:00:00.000Z'
520
+ })
521
+
522
+ const networkError = new Error('network meltdown')
523
+ getPaymentStatus.mockRejectedValueOnce(networkError)
524
+
525
+ await processRecurringPayments()
526
+
527
+ expect(debugLogger).toHaveBeenCalledWith(`Unexpected error fetching payment status for ${mockPaymentId}.`)
363
528
  })
364
529
 
365
530
  it('should call setTimeout with correct delay when there are recurring payments', async () => {
@@ -387,6 +552,9 @@ describe('recurring-payments-processor', () => {
387
552
  })
388
553
 
389
554
  it('calls processRPResult with transaction id, payment id and created date when payment is successful', async () => {
555
+ debugLogger.mockImplementation(function () {
556
+ console.log(...arguments)
557
+ })
390
558
  const mockTransactionId = 'test-transaction-id'
391
559
  const mockPaymentId = 'test-payment-id'
392
560
  const mockPaymentCreatedDate = '2025-01-01T00:00:00.000Z'
@@ -397,6 +565,7 @@ describe('recurring-payments-processor', () => {
397
565
 
398
566
  await processRecurringPayments()
399
567
 
568
+ console.log(salesApi.processRPResult.mock.calls, mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
400
569
  expect(salesApi.processRPResult).toHaveBeenCalledWith(mockTransactionId, mockPaymentId, mockPaymentCreatedDate)
401
570
  })
402
571
 
@@ -586,7 +755,7 @@ describe('recurring-payments-processor', () => {
586
755
 
587
756
  const permits = []
588
757
  for (let i = 0; i < count; i++) {
589
- permits.push(`permit${i}`)
758
+ permits.push(Symbol(`permit${i}`))
590
759
  }
591
760
 
592
761
  permits.forEach((permit, i) => {
@@ -615,8 +784,3 @@ describe('recurring-payments-processor', () => {
615
784
  })
616
785
  })
617
786
  })
618
-
619
- const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = 'test-agreement-id') => ({
620
- entity: { agreementId },
621
- expanded: { activePermission: { entity: { referenceNumber } } }
622
- })
@@ -1,63 +1,91 @@
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
3
  import { salesApi } from '@defra-fish/connectors-lib'
4
- import { getPaymentStatus, sendPayment } from './services/govuk-pay-service.js'
4
+ import { getPaymentStatus, sendPayment, isGovPayUp } from './services/govuk-pay-service.js'
5
5
  import db from 'debug'
6
+
6
7
  const debug = db('recurring-payments:processor')
7
8
 
8
9
  const PAYMENT_STATUS_DELAY = 60000
9
- const payments = []
10
+ const MIN_CLIENT_ERROR = 400
11
+ const MAX_CLIENT_ERROR = 499
12
+ const MIN_SERVER_ERROR = 500
13
+ const MAX_SERVER_ERROR = 599
14
+
15
+ const isClientError = code => code >= MIN_CLIENT_ERROR && code <= MAX_CLIENT_ERROR
16
+ const isServerError = code => code >= MIN_SERVER_ERROR && code <= MAX_SERVER_ERROR
17
+
18
+ const fetchDueRecurringPayments = async date => {
19
+ 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
26
+ }
27
+ }
10
28
 
11
29
  export const processRecurringPayments = async () => {
12
- if (process.env.RUN_RECURRING_PAYMENTS?.toLowerCase() === 'true') {
13
- debug('Recurring Payments job enabled')
14
- const date = new Date().toISOString().split('T')[0]
15
- const response = await salesApi.getDueRecurringPayments(date)
16
- debug('Recurring Payments found: ', response)
17
- await Promise.all(response.map(record => processRecurringPayment(record)))
18
- if (response.length > 0) {
19
- await new Promise(resolve => setTimeout(resolve, PAYMENT_STATUS_DELAY))
20
- await Promise.all(response.map(record => processRecurringPaymentStatus(record)))
21
- }
22
- } else {
30
+ if (process.env.RUN_RECURRING_PAYMENTS?.toLowerCase() !== 'true') {
23
31
  debug('Recurring Payments job disabled')
32
+ return
33
+ }
34
+
35
+ if (!(await isGovPayUp())) {
36
+ debug('Gov.UK Pay reporting unhealthy, aborting run')
37
+ throw new Error('Run aborted, Gov.UK Pay health endpoint is reporting problems.')
24
38
  }
39
+
40
+ debug('Recurring Payments job enabled')
41
+ const date = new Date().toISOString().split('T')[0]
42
+
43
+ const dueRCPayments = await fetchDueRecurringPayments(date)
44
+ if (dueRCPayments.length === 0) {
45
+ return
46
+ }
47
+
48
+ const payments = await requestPayments(dueRCPayments)
49
+
50
+ await new Promise(resolve => setTimeout(resolve, PAYMENT_STATUS_DELAY))
51
+
52
+ await Promise.allSettled(payments.map(p => processRecurringPaymentStatus(p)))
53
+ }
54
+
55
+ const requestPayments = async dueRCPayments => {
56
+ const paymentRequestResults = await Promise.allSettled(dueRCPayments.map(processRecurringPayment))
57
+ const payments = paymentRequestResults.filter(prr => prr.status === 'fulfilled').map(p => p.value)
58
+ const failures = paymentRequestResults.filter(prr => prr.status === 'rejected').map(f => f.reason)
59
+ if (failures.length) {
60
+ debug('Error requesting payments:', ...failures)
61
+ }
62
+ return payments
25
63
  }
26
64
 
27
65
  const processRecurringPayment = async record => {
28
66
  const referenceNumber = record.expanded.activePermission.entity.referenceNumber
29
67
  const agreementId = record.entity.agreementId
30
68
  const transaction = await createNewTransaction(referenceNumber, agreementId)
31
- await takeRecurringPayment(agreementId, transaction)
69
+ return takeRecurringPayment(agreementId, transaction)
32
70
  }
33
71
 
34
72
  const createNewTransaction = async (referenceNumber, agreementId) => {
35
73
  const transactionData = await processPermissionData(referenceNumber, agreementId)
36
- debug('Creating new transaction based on: ', referenceNumber, 'with agreementId: ', agreementId)
37
- try {
38
- const response = await salesApi.createTransaction(transactionData)
39
- debug('New transaction created:', response)
40
- return response
41
- } catch (e) {
42
- console.error('Error creating transaction', JSON.stringify(transactionData))
43
- throw e
44
- }
74
+ return salesApi.createTransaction(transactionData)
45
75
  }
46
76
 
47
77
  const takeRecurringPayment = async (agreementId, transaction) => {
48
78
  const preparedPayment = preparePayment(agreementId, transaction)
49
- debug('Requesting payment:', preparedPayment)
50
79
  const payment = await sendPayment(preparedPayment)
51
- payments.push({
80
+ return {
52
81
  agreementId,
53
82
  paymentId: payment.payment_id,
54
83
  created_date: payment.created_date,
55
84
  transaction
56
- })
85
+ }
57
86
  }
58
87
 
59
88
  const processPermissionData = async (referenceNumber, agreementId) => {
60
- debug('Preparing data based on', referenceNumber, 'with agreementId', agreementId)
61
89
  const data = await salesApi.preparePermissionDataForRenewal(referenceNumber)
62
90
  const licenseeWithoutCountryCode = Object.assign((({ countryCode: _countryCode, ...l }) => l)(data.licensee))
63
91
  return {
@@ -99,28 +127,37 @@ const preparePayment = (agreementId, transaction) => {
99
127
  return result
100
128
  }
101
129
 
102
- const processRecurringPaymentStatus = async record => {
103
- const agreementId = record.entity.agreementId
104
- const paymentId = getPaymentId(agreementId)
105
- const {
106
- state: { status }
107
- } = await getPaymentStatus(paymentId)
108
- debug(`Payment status for ${paymentId}: ${status}`)
109
- const payment = payments.find(p => p.paymentId === paymentId)
110
- if (status === PAYMENT_STATUS.Success) {
111
- await salesApi.processRPResult(payment.transaction.id, paymentId, payment.created_date)
112
- debug(`Processed Recurring Payment for ${payment.transaction.id}`)
113
- }
114
- if (status === PAYMENT_STATUS.Failure || status === PAYMENT_STATUS.Error) {
115
- console.error(`Payment failed. Recurring payment agreement for: ${agreementId} set to be cancelled. Updating payment journal.`)
116
- if (await salesApi.getPaymentJournal(payment.transaction.id)) {
117
- await salesApi.updatePaymentJournal(payment.transaction.id, {
118
- paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed
119
- })
130
+ const processRecurringPaymentStatus = async payment => {
131
+ try {
132
+ const {
133
+ state: { status }
134
+ } = await getPaymentStatus(payment.paymentId)
135
+
136
+ debug(`Payment status for ${payment.paymentId}: ${status}`)
137
+
138
+ if (status === PAYMENT_STATUS.Success) {
139
+ await salesApi.processRPResult(payment.transaction.id, payment.paymentId, payment.created_date)
140
+ debug(`Processed Recurring Payment for ${payment.transaction.id}`)
120
141
  }
121
- }
122
- }
142
+ if (status === PAYMENT_STATUS.Failure || status === PAYMENT_STATUS.Error) {
143
+ console.error(
144
+ `Payment failed. Recurring payment agreement for: ${payment.agreementId} set to be cancelled. Updating payment journal.`
145
+ )
146
+ if (await salesApi.getPaymentJournal(payment.transaction.id)) {
147
+ await salesApi.updatePaymentJournal(payment.transaction.id, {
148
+ paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed
149
+ })
150
+ }
151
+ }
152
+ } catch (error) {
153
+ const status = error.response?.status
123
154
 
124
- const getPaymentId = agreementId => {
125
- return payments.find(p => p.agreementId === agreementId).paymentId
155
+ if (isClientError(status)) {
156
+ debug(`Failed to fetch status for payment ${payment.paymentId}, error ${status}`)
157
+ } else if (isServerError(status)) {
158
+ debug(`Payment status API error for ${payment.paymentId}, error ${status}`)
159
+ } else {
160
+ debug(`Unexpected error fetching payment status for ${payment.paymentId}.`)
161
+ }
162
+ }
126
163
  }
@@ -1,9 +1,16 @@
1
- import { getPaymentStatus, sendPayment } from '../govuk-pay-service.js'
1
+ import { getPaymentStatus, sendPayment, isGovPayUp } from '../govuk-pay-service.js'
2
2
  import { govUkPayApi } from '@defra-fish/connectors-lib'
3
+ import db from 'debug'
3
4
 
4
5
  jest.mock('@defra-fish/connectors-lib')
6
+ jest.mock('debug', () => jest.fn(() => jest.fn()))
7
+ const mockDebug = db.mock.results[0].value
5
8
 
6
9
  describe('govuk-pay-service', () => {
10
+ it('initialises logger', () => {
11
+ expect(db).toHaveBeenCalledWith('recurring-payments:gov.uk-pay-service')
12
+ })
13
+
7
14
  describe('sendPayment', () => {
8
15
  const preparedPayment = { id: '1234' }
9
16
 
@@ -117,4 +124,34 @@ describe('govuk-pay-service', () => {
117
124
  await expect(getPaymentStatus('test-payment-id')).rejects.toThrow('Network error')
118
125
  })
119
126
  })
127
+
128
+ describe('isGovPayUp', () => {
129
+ it.each([
130
+ [true, 'true', 'true'],
131
+ [false, 'true', 'false'],
132
+ [false, 'false', 'true'],
133
+ [false, 'false', 'false']
134
+ ])('resolves to %p if healthy is %s and deadlocks is %s', async (expectedResult, pingHealthy, deadlocksHealthy) => {
135
+ govUkPayApi.isGovPayUp.mockResolvedValueOnce({
136
+ ok: true,
137
+ text: async () => `{"ping":{"healthy":${pingHealthy}},"deadlocks":{"healthy":${deadlocksHealthy}}}`
138
+ })
139
+ expect(await isGovPayUp()).toBe(expectedResult)
140
+ })
141
+
142
+ it("resolves to false if we don't receive a 2xx response", async () => {
143
+ govUkPayApi.isGovPayUp.mockResolvedValueOnce({
144
+ ok: false
145
+ })
146
+ expect(await isGovPayUp()).toBe(false)
147
+ })
148
+
149
+ it("logs if we don't receive a 2xx response", async () => {
150
+ govUkPayApi.isGovPayUp.mockResolvedValueOnce({
151
+ ok: false
152
+ })
153
+ await isGovPayUp()
154
+ expect(mockDebug).toHaveBeenCalledWith('Health endpoint unavailable')
155
+ })
156
+ })
120
157
  })
@@ -1,4 +1,6 @@
1
1
  import { govUkPayApi } from '@defra-fish/connectors-lib'
2
+ import db from 'debug'
3
+ const debug = db('recurring-payments:gov.uk-pay-service')
2
4
 
3
5
  export const sendPayment = async preparedPayment => {
4
6
  try {
@@ -31,3 +33,13 @@ export const getPaymentStatus = async paymentId => {
31
33
  throw error
32
34
  }
33
35
  }
36
+
37
+ export const isGovPayUp = async () => {
38
+ const response = await govUkPayApi.isGovPayUp()
39
+ if (response.ok) {
40
+ const isHealthy = JSON.parse(await response.text())
41
+ return isHealthy.ping.healthy && isHealthy.deadlocks.healthy
42
+ }
43
+ debug('Health endpoint unavailable')
44
+ return false
45
+ }