@defra-fish/recurring-payments-job 1.64.0-rc.1 → 1.64.0-rc.10

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/README.md CHANGED
@@ -8,21 +8,13 @@ When the RP job runs, all RP entries with an nextDueDate of the current date, a
8
8
 
9
9
  # Environment variables
10
10
 
11
- | name | description | required | default | valid | notes |
12
- | ------------------------------ | --------------------------------------------------------------- | :------: | ------------------- | ----------------------------- | --------------------------------------------------------------------------------- | --- |
13
- | NODE_ENV | Node environment | no | | development, test, production | |
14
- | RUN_RECURRING_PAYMENTS | Determine whether to run recurring payments job or not | yes | | | |
15
- | SALES_API_URL | URL for the sales API | no | http://0.0.0.0:4000 | | |
16
- | SALES_API_TIMEOUT_MS | The timeout in milliseconds requests to the API | no | 10000 | | |
17
- | OAUTH_AUTHORITY_HOST_URL | OAuth 2.0 authority host | yes | | | |
18
- | OAUTH_TENANT | OAuth 2.0 tenant | yes | | | |
19
- | OAUTH_CLIENT_ID | OAuth 2.0 client ID for client credentials flow | yes | | | |
20
- | OAUTH_CLIENT_SECRET | OAuth 2.0 client secret for client credentials flow | yes | | | |
21
- | OAUTH_SCOPE | OAuth 2.0 scope to request (client credentials resource) | yes | | | |
22
- | DYNAMICS_API_PATH | Full URL to the Dynamics API | yes | | | The full URL to the dynamics web api. e.g. https://dynamics-server/api/data/v9.1/ |
23
- | DYNAMICS_API_VERSION | The version of the Dynamics API | yes | | | The version of the dynamics web api. e.g. 9.1 |
24
- | 1 |
25
- | RECURRING_PAYMENTS_LOCAL_DELAY | Delay for running recurring payments until sales api is running | no | | | | |
11
+ | name | description | required | default | valid | notes |
12
+ | ------------------------------ | --------------------------------------------------------------- | :------: | ------------------- | ----------------------------- | ----- |
13
+ | NODE_ENV | Node environment | no | | development, test, production | |
14
+ | RUN_RECURRING_PAYMENTS | Determine whether to run recurring payments job or not | yes | | | |
15
+ | SALES_API_URL | URL for the sales API | no | http://0.0.0.0:4000 | | |
16
+ | SALES_API_TIMEOUT_MS | The timeout in milliseconds requests to the API | no | 10000 | | |
17
+ | RECURRING_PAYMENTS_LOCAL_DELAY | Delay for running recurring payments until sales api is running | no | | | |
26
18
 
27
19
  ### See also:
28
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra-fish/recurring-payments-job",
3
- "version": "1.64.0-rc.1",
3
+ "version": "1.64.0-rc.10",
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.64.0-rc.1",
40
- "@defra-fish/connectors-lib": "1.64.0-rc.1",
41
- "commander": "^7.2.0",
42
- "debug": "^4.3.3",
43
- "moment-timezone": "^0.5.34"
39
+ "@defra-fish/business-rules-lib": "1.64.0-rc.10",
40
+ "@defra-fish/connectors-lib": "1.64.0-rc.10",
41
+ "commander": "7.2.0",
42
+ "debug": "4.3.3",
43
+ "moment-timezone": "0.5.34"
44
44
  },
45
- "gitHead": "2d114ca497f84cd7ede12c2f93965a7044d2f39a"
45
+ "gitHead": "c70266a7758feee3a9eb9a6b68304d767c4929ce"
46
46
  }
@@ -70,6 +70,13 @@ const getMockDueRecurringPayment = ({ agreementId = 'test-agreement-id', id = 'a
70
70
  expanded: { activePermission: { entity: { referenceNumber } } }
71
71
  })
72
72
 
73
+ // eslint-disable-next-line camelcase
74
+ const getMockSendPaymentResponse = ({ payment_id = 'pay-1', agreementId = 'agr-1', created_date = '2025-01-01T00:00:00.000Z' } = {}) => ({
75
+ payment_id,
76
+ agreementId,
77
+ created_date
78
+ })
79
+
73
80
  describe('recurring-payments-processor', () => {
74
81
  const [{ value: debugLogger }] = db.mock.results
75
82
 
@@ -209,7 +216,8 @@ describe('recurring-payments-processor', () => {
209
216
  })
210
217
 
211
218
  describe('When payment request throws an error...', () => {
212
- it('debug is called with error message', async () => {
219
+ it('console.error is called with error message', async () => {
220
+ jest.spyOn(console, 'error')
213
221
  salesApi.getDueRecurringPayments.mockReturnValueOnce(getMockPaymentRequestResponse())
214
222
  const oopsie = new Error('payment gate down')
215
223
  sendPayment.mockRejectedValueOnce(oopsie)
@@ -218,7 +226,7 @@ describe('recurring-payments-processor', () => {
218
226
  await execute()
219
227
  } catch {}
220
228
 
221
- expect(debugLogger).toHaveBeenCalledWith(expect.any(String), oopsie)
229
+ expect(console.error).toHaveBeenCalledWith(expect.any(String), oopsie)
222
230
  })
223
231
 
224
232
  it('prepares and sends all payment requests, even if some fail', async () => {
@@ -277,6 +285,7 @@ describe('recurring-payments-processor', () => {
277
285
  })
278
286
 
279
287
  it('logs an error for every failure', async () => {
288
+ jest.spyOn(console, 'error')
280
289
  const errors = [new Error('error 1'), new Error('error 2'), new Error('error 3')]
281
290
  salesApi.getDueRecurringPayments.mockReturnValueOnce([
282
291
  getMockDueRecurringPayment({ referenceNumber: 'fee', agreementId: 'a1' }),
@@ -293,7 +302,7 @@ describe('recurring-payments-processor', () => {
293
302
 
294
303
  await execute()
295
304
 
296
- expect(debugLogger).toHaveBeenCalledWith(expect.any(String), ...errors)
305
+ expect(console.error).toHaveBeenCalledWith(expect.any(String), ...errors)
297
306
  })
298
307
  })
299
308
 
@@ -577,6 +586,7 @@ describe('recurring-payments-processor', () => {
577
586
  })
578
587
 
579
588
  it('logs an error if createTransaction fails', async () => {
589
+ jest.spyOn(console, 'error')
580
590
  salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
581
591
  const error = new Error('Wuh-oh!')
582
592
  salesApi.createTransaction.mockImplementationOnce(() => {
@@ -585,9 +595,142 @@ describe('recurring-payments-processor', () => {
585
595
 
586
596
  await execute()
587
597
 
588
- expect(debugLogger).toHaveBeenCalledWith(expect.any(String), error)
598
+ expect(console.error).toHaveBeenCalledWith(expect.any(String), error)
599
+ })
600
+
601
+ // --- //
602
+
603
+ it('should log errors from await salesApi.processRPResult', async () => {
604
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment()])
605
+ salesApi.createTransaction.mockResolvedValueOnce({ id: 'trans-1', cost: 30 })
606
+
607
+ const payment = getMockSendPaymentResponse()
608
+ sendPayment.mockResolvedValueOnce(payment)
609
+
610
+ getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
611
+
612
+ const boom = new Error('boom')
613
+
614
+ salesApi.processRPResult.mockImplementation(transId => (transId === 'trans-1' ? Promise.reject(boom) : Promise.resolve()))
615
+
616
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
617
+
618
+ await execute()
619
+
620
+ expect(errorSpy).toHaveBeenCalledWith('Failed to process Recurring Payment for trans-1', boom)
621
+
622
+ errorSpy.mockRestore()
589
623
  })
590
624
 
625
+ describe('handling failures for multiple due payments', () => {
626
+ beforeEach(() => {
627
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment(), getMockDueRecurringPayment()])
628
+
629
+ salesApi.preparePermissionDataForRenewal.mockResolvedValueOnce({ licensee: { countryCode: 'GB-ENG' } })
630
+
631
+ salesApi.createTransaction.mockResolvedValueOnce({ id: 'trans-1', cost: 30 }).mockResolvedValueOnce({ id: 'trans-2', cost: 30 })
632
+ })
633
+
634
+ it('continues when one sendPayment rejects (Promise.allSettled check)', async () => {
635
+ const secondPayment = getMockSendPaymentResponse({
636
+ payment_id: 'test-payment-second',
637
+ agreementId: 'agr-2',
638
+ created_date: '2025-01-01T00:00:00.000Z'
639
+ })
640
+
641
+ const gatewayDown = new Error('gateway down')
642
+ sendPayment.mockRejectedValueOnce(gatewayDown).mockResolvedValueOnce(secondPayment)
643
+ getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
644
+ salesApi.processRPResult.mockResolvedValueOnce()
645
+
646
+ await execute()
647
+
648
+ const summary = {
649
+ statusArgs: getPaymentStatus.mock.calls,
650
+ rpResultArgs: salesApi.processRPResult.mock.calls
651
+ }
652
+
653
+ expect(summary).toEqual({
654
+ statusArgs: [[secondPayment.payment_id]],
655
+ rpResultArgs: [['trans-2', secondPayment.payment_id, secondPayment.created_date]]
656
+ })
657
+ })
658
+
659
+ it('continues when processRPResult rejects for one payment', async () => {
660
+ const firstPayment = getMockSendPaymentResponse({
661
+ payment_id: 'pay-1',
662
+ agreementId: 'agr-1',
663
+ created_date: '2025-01-01T00:00:00.000Z'
664
+ })
665
+ const secondPayment = getMockSendPaymentResponse({
666
+ payment_id: 'pay-2',
667
+ agreementId: 'agr-2',
668
+ created_date: '2025-01-01T00:01:00.000Z'
669
+ })
670
+
671
+ sendPayment.mockResolvedValueOnce(firstPayment).mockResolvedValueOnce(secondPayment)
672
+ getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess()).mockResolvedValueOnce(getPaymentStatusSuccess())
673
+
674
+ const boom = new Error('boom')
675
+ salesApi.processRPResult.mockImplementation(transId => (transId === 'trans-1' ? Promise.reject(boom) : Promise.resolve()))
676
+
677
+ const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
678
+
679
+ await execute()
680
+
681
+ const summary = {
682
+ rpResultArgs: salesApi.processRPResult.mock.calls,
683
+ rpCount: salesApi.processRPResult.mock.calls.length,
684
+ firstError: errorSpy.mock.calls[0]
685
+ }
686
+
687
+ errorSpy.mockRestore()
688
+
689
+ expect(summary).toEqual({
690
+ rpResultArgs: expect.arrayContaining([
691
+ ['trans-1', firstPayment.payment_id, firstPayment.created_date],
692
+ ['trans-2', secondPayment.payment_id, secondPayment.created_date]
693
+ ]),
694
+ rpCount: 2,
695
+ firstError: ['Failed to process Recurring Payment for trans-1', boom]
696
+ })
697
+ })
698
+
699
+ it('does not abort when getPaymentStatus rejects for one payment (allSettled at status stage)', async () => {
700
+ const p1 = getMockSendPaymentResponse({ payment_id: 'pay-1', created_date: '2025-01-01T00:00:00.000Z' })
701
+ const p2 = getMockSendPaymentResponse({ payment_id: 'pay-2', created_date: '2025-01-01T00:01:00.000Z' })
702
+
703
+ sendPayment.mockResolvedValueOnce(p1).mockResolvedValueOnce(p2)
704
+
705
+ getPaymentStatus.mockImplementation(async id => {
706
+ if (id === p1.payment_id) {
707
+ throw Object.assign(new Error('HTTP 500'), { response: { status: 500, data: 'boom' } })
708
+ }
709
+ return getPaymentStatusSuccess()
710
+ })
711
+
712
+ salesApi.processRPResult.mockResolvedValueOnce()
713
+
714
+ await execute()
715
+
716
+ const summary = {
717
+ statusArgs: getPaymentStatus.mock.calls,
718
+ statusCount: getPaymentStatus.mock.calls.length,
719
+ rpResultArgs: salesApi.processRPResult.mock.calls,
720
+ rpCount: salesApi.processRPResult.mock.calls.length
721
+ }
722
+
723
+ expect(summary).toEqual({
724
+ statusArgs: expect.arrayContaining([[p1.payment_id], [p2.payment_id]]),
725
+ statusCount: 2,
726
+ rpResultArgs: expect.arrayContaining([['trans-2', p2.payment_id, p2.created_date]]),
727
+ rpCount: 1
728
+ })
729
+ })
730
+ })
731
+
732
+ // --- //
733
+
591
734
  it.each([
592
735
  [400, 'Failed to fetch status for payment test-payment-id, error 400'],
593
736
  [486, 'Failed to fetch status for payment test-payment-id, error 486'],
@@ -596,6 +739,7 @@ describe('recurring-payments-processor', () => {
596
739
  [512, 'Payment status API error for test-payment-id, error 512'],
597
740
  [599, 'Payment status API error for test-payment-id, error 599']
598
741
  ])('logs the correct message when getPaymentStatus rejects with HTTP %i', async (statusCode, expectedMessage) => {
742
+ jest.spyOn(console, 'error')
599
743
  const mockPaymentId = 'test-payment-id'
600
744
  const mockResponse = [
601
745
  { entity: { agreementId: 'agreement-1' }, expanded: { activePermission: { entity: { referenceNumber: 'ref-1' } } } }
@@ -613,10 +757,11 @@ describe('recurring-payments-processor', () => {
613
757
 
614
758
  await execute()
615
759
 
616
- expect(debugLogger).toHaveBeenCalledWith(expectedMessage)
760
+ expect(console.error).toHaveBeenCalledWith(expectedMessage)
617
761
  })
618
762
 
619
763
  it('logs the generic unexpected-error message and still rejects', async () => {
764
+ jest.spyOn(console, 'error')
620
765
  const mockPaymentId = 'test-payment-id'
621
766
  salesApi.getDueRecurringPayments.mockResolvedValueOnce(getMockPaymentRequestResponse())
622
767
  salesApi.createTransaction.mockResolvedValueOnce({ id: mockPaymentId })
@@ -631,7 +776,7 @@ describe('recurring-payments-processor', () => {
631
776
 
632
777
  await execute()
633
778
 
634
- expect(debugLogger).toHaveBeenCalledWith(`Unexpected error fetching payment status for ${mockPaymentId}.`)
779
+ expect(console.error).toHaveBeenCalledWith(`Unexpected error fetching payment status for ${mockPaymentId}.`)
635
780
  })
636
781
 
637
782
  it('should call setTimeout with correct delay when there are recurring payments', async () => {
@@ -70,7 +70,7 @@ const requestPayments = async dueRCPayments => {
70
70
  const payments = paymentRequestResults.filter(prr => prr.status === 'fulfilled').map(p => p.value)
71
71
  const failures = paymentRequestResults.filter(prr => prr.status === 'rejected').map(f => f.reason)
72
72
  if (failures.length) {
73
- debug('Error requesting payments:', ...failures)
73
+ console.error('Error requesting payments:', ...failures)
74
74
  }
75
75
  return payments
76
76
  }
@@ -155,8 +155,13 @@ const processRecurringPaymentStatus = async payment => {
155
155
  debug(`Payment status for ${payment.paymentId}: ${status}`)
156
156
 
157
157
  if (status === PAYMENT_STATUS.Success) {
158
- await salesApi.processRPResult(payment.transaction.id, payment.paymentId, payment.created_date)
159
- debug(`Processed Recurring Payment for ${payment.transaction.id}`)
158
+ try {
159
+ await salesApi.processRPResult(payment.transaction.id, payment.paymentId, payment.created_date)
160
+ debug(`Processed Recurring Payment for ${payment.transaction.id}`)
161
+ } catch (err) {
162
+ console.error(`Failed to process Recurring Payment for ${payment.transaction.id}`, err)
163
+ throw err
164
+ }
160
165
  }
161
166
  if (status === PAYMENT_STATUS.Failure || status === PAYMENT_STATUS.Error) {
162
167
  console.error(
@@ -173,11 +178,11 @@ const processRecurringPaymentStatus = async payment => {
173
178
  const status = error.response?.status
174
179
 
175
180
  if (isClientError(status)) {
176
- debug(`Failed to fetch status for payment ${payment.paymentId}, error ${status}`)
181
+ console.error(`Failed to fetch status for payment ${payment.paymentId}, error ${status}`)
177
182
  } else if (isServerError(status)) {
178
- debug(`Payment status API error for ${payment.paymentId}, error ${status}`)
183
+ console.error(`Payment status API error for ${payment.paymentId}, error ${status}`)
179
184
  } else {
180
- debug(`Unexpected error fetching payment status for ${payment.paymentId}.`)
185
+ console.error(`Unexpected error fetching payment status for ${payment.paymentId}.`)
181
186
  }
182
187
  }
183
188
  }
@@ -57,19 +57,6 @@ describe('govuk-pay-service', () => {
57
57
  }
58
58
  })
59
59
 
60
- it('should log error message when the GOV.UK Pay API raises an error', async () => {
61
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
62
- govUkPayApi.createPayment.mockImplementationOnce(() => {
63
- throw new Error()
64
- })
65
-
66
- try {
67
- await sendPayment(preparedPayment)
68
- } catch (error) {
69
- expect(consoleSpy).toHaveBeenCalledWith('Error creating payment', preparedPayment.id)
70
- }
71
- })
72
-
73
60
  it('should throw an error when response is not ok', async () => {
74
61
  const mockFetchResponse = {
75
62
  ok: false,
@@ -92,43 +79,6 @@ describe('govuk-pay-service', () => {
92
79
  })
93
80
  ).rejects.toThrow('Unexpected response from GOV.UK Pay API')
94
81
  })
95
-
96
- it('should log details when response is not ok', async () => {
97
- const status = 400
98
- const serviceResponseBody = {
99
- code: 'P0102',
100
- field: 'agreement_id',
101
- description: 'Invalid attribute value: agreement_id. Agreement does not exist'
102
- }
103
- const transactionId = 'a50f0d51-295f-42b3-98f8-97c0641ede5a'
104
- const preparedPayment = {
105
- amount: 100,
106
- description: 'The recurring card payment for your rod fishing licence',
107
- id: transactionId,
108
- authorisation_mode: 'agreement',
109
- agreement_id: 'does_not_exist'
110
- }
111
- govUkPayApi.createPayment.mockResolvedValueOnce({
112
- ok: false,
113
- status,
114
- json: jest.fn().mockResolvedValue(serviceResponseBody)
115
- })
116
- jest.spyOn(console, 'error')
117
-
118
- try {
119
- await sendPayment(preparedPayment)
120
- } catch {}
121
-
122
- expect(console.error).toHaveBeenCalledWith(
123
- expect.objectContaining({
124
- method: 'POST',
125
- status,
126
- response: serviceResponseBody,
127
- transactionId,
128
- payload: preparedPayment
129
- })
130
- )
131
- })
132
82
  })
133
83
 
134
84
  describe('getPaymentStatus', () => {
@@ -3,24 +3,15 @@ import db from 'debug'
3
3
  const debug = db('recurring-payments:gov.uk-pay-service')
4
4
 
5
5
  export const sendPayment = async preparedPayment => {
6
- const createPayment = async () => {
7
- try {
8
- return await govUkPayApi.createPayment(preparedPayment, true)
9
- } catch (e) {
10
- console.error('Error creating payment', preparedPayment.id)
11
- throw e
12
- }
13
- }
6
+ const createPayment = () => govUkPayApi.createPayment(preparedPayment, true)
14
7
  const response = await createPayment()
15
8
  if (!response.ok) {
16
- console.error({
17
- method: 'POST',
18
- status: response.status,
19
- response: await response.json(),
20
- transactionId: preparedPayment.id,
21
- payload: preparedPayment
22
- })
23
- throw new Error('Unexpected response from GOV.UK Pay API')
9
+ throw new Error(`Unexpected response from GOV.UK Pay API.
10
+ Status: ${response.status},
11
+ Response: ${JSON.stringify(await response.json())}
12
+ Transaction ID: ${preparedPayment.id}
13
+ Payload: ${JSON.stringify(preparedPayment)}
14
+ `)
24
15
  }
25
16
  return response.json()
26
17
  }