@defra-fish/recurring-payments-job 1.63.0-rc.1 → 1.63.0-rc.11

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.1",
3
+ "version": "1.63.0-rc.11",
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.1",
40
- "@defra-fish/connectors-lib": "1.63.0-rc.1",
39
+ "@defra-fish/business-rules-lib": "1.63.0-rc.11",
40
+ "@defra-fish/connectors-lib": "1.63.0-rc.11",
41
41
  "commander": "^7.2.0",
42
42
  "debug": "^4.3.3",
43
43
  "moment-timezone": "^0.5.34"
44
44
  },
45
- "gitHead": "93c7097feecf01ce2d207cafe43227f7aaa77376"
45
+ "gitHead": "7d0e5dab76d8aebb0dbaecb857c3d44fdc6f79f9"
46
46
  }
@@ -1,5 +1,6 @@
1
1
  import commander from 'commander'
2
2
  import { processRecurringPayments } from '../recurring-payments-processor.js'
3
+ import fs from 'fs'
3
4
 
4
5
  jest.useFakeTimers()
5
6
 
@@ -17,12 +18,30 @@ jest.mock('commander', () => {
17
18
  return global.commander
18
19
  })
19
20
 
21
+ jest.mock('fs')
22
+
20
23
  describe('recurring-payments-job', () => {
21
24
  beforeEach(() => {
22
25
  jest.clearAllMocks()
23
26
  commander.args = ['test']
24
27
  })
25
28
 
29
+ it('logs startup details including name and version', () => {
30
+ const mockPkg = { name: 'recurring-payments-test', version: '1.2.3' }
31
+ fs.readFileSync.mockReturnValue(JSON.stringify(mockPkg))
32
+
33
+ jest.isolateModules(() => {
34
+ const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
35
+ require('../recurring-payments-job.js')
36
+ expect(logSpy).toHaveBeenCalledWith(
37
+ 'Recurring payments job starting at %s. name: %s. version: %s',
38
+ expect.any(String),
39
+ mockPkg.name,
40
+ mockPkg.version
41
+ )
42
+ })
43
+ })
44
+
26
45
  it('calls processRecurringPayments when no delay', () => {
27
46
  jest.isolateModules(() => {
28
47
  require('../recurring-payments-job.js')
@@ -20,17 +20,19 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
20
20
  }))
21
21
  jest.mock('@defra-fish/connectors-lib', () => ({
22
22
  salesApi: {
23
- getDueRecurringPayments: jest.fn(() => []),
24
- preparePermissionDataForRenewal: jest.fn(() => ({
25
- licensee: { countryCode: 'GB-ENG' }
26
- })),
23
+ cancelRecurringPayment: jest.fn(),
24
+ createPaymentJournal: jest.fn(),
27
25
  createTransaction: jest.fn(() => ({
28
26
  id: 'test-transaction-id',
29
27
  cost: 30
30
28
  })),
29
+ getDueRecurringPayments: jest.fn(() => []),
30
+ getPaymentJournal: jest.fn(),
31
+ preparePermissionDataForRenewal: jest.fn(() => ({
32
+ licensee: { countryCode: 'GB-ENG' }
33
+ })),
31
34
  processRPResult: jest.fn(),
32
- updatePaymentJournal: jest.fn(),
33
- getPaymentJournal: jest.fn()
35
+ updatePaymentJournal: jest.fn()
34
36
  }
35
37
  }))
36
38
 
@@ -59,8 +61,8 @@ const getMockPaymentRequestResponse = () => [
59
61
  }
60
62
  ]
61
63
 
62
- const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = 'test-agreement-id') => ({
63
- entity: { agreementId },
64
+ const getMockDueRecurringPayment = ({ agreementId = 'test-agreement-id', id = 'abc-123', referenceNumber = '123' } = {}) => ({
65
+ entity: { id, agreementId },
64
66
  expanded: { activePermission: { entity: { referenceNumber } } }
65
67
  })
66
68
 
@@ -147,10 +149,10 @@ describe('recurring-payments-processor', () => {
147
149
  it('prepares and sends all payment requests, even if some fail', async () => {
148
150
  const agreementIds = [Symbol('agreementId1'), Symbol('agreementId2'), Symbol('agreementId3'), Symbol('agreementId4')]
149
151
  salesApi.getDueRecurringPayments.mockReturnValueOnce([
150
- getMockDueRecurringPayment('fee', agreementIds[0]),
151
- getMockDueRecurringPayment('fi', agreementIds[1]),
152
- getMockDueRecurringPayment('foe', agreementIds[2]),
153
- getMockDueRecurringPayment('fum', agreementIds[3])
152
+ getMockDueRecurringPayment({ referenceNumber: 'fee', agreementId: agreementIds[0] }),
153
+ getMockDueRecurringPayment({ referenceNumber: 'fi', agreementId: agreementIds[1] }),
154
+ getMockDueRecurringPayment({ referenceNumber: 'foe', agreementId: agreementIds[2] }),
155
+ getMockDueRecurringPayment({ referenceNumber: 'fum', agreementId: agreementIds[3] })
154
156
  ])
155
157
 
156
158
  const permissionData = { licensee: { countryCode: 'GB-ENG' } }
@@ -202,9 +204,9 @@ describe('recurring-payments-processor', () => {
202
204
  it('logs an error for every failure', async () => {
203
205
  const errors = [new Error('error 1'), new Error('error 2'), new Error('error 3')]
204
206
  salesApi.getDueRecurringPayments.mockReturnValueOnce([
205
- getMockDueRecurringPayment('fee', 'a1'),
206
- getMockDueRecurringPayment('fi', 'a2'),
207
- getMockDueRecurringPayment('foe', 'a3')
207
+ getMockDueRecurringPayment({ referenceNumber: 'fee', agreementId: 'a1' }),
208
+ getMockDueRecurringPayment({ referenceNumber: 'fi', agreementId: 'a2' }),
209
+ getMockDueRecurringPayment({ referenceNumber: 'foe', agreementId: 'a3' })
208
210
  ])
209
211
  const permissionData = { licensee: { countryCode: 'GB-ENG' } }
210
212
  salesApi.preparePermissionDataForRenewal
@@ -241,7 +243,7 @@ describe('recurring-payments-processor', () => {
241
243
 
242
244
  it('prepares the data for found recurring payments', async () => {
243
245
  const referenceNumber = Symbol('reference')
244
- salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment(referenceNumber)])
246
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ referenceNumber })])
245
247
  const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
246
248
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
247
249
  getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
@@ -252,7 +254,9 @@ describe('recurring-payments-processor', () => {
252
254
  })
253
255
 
254
256
  it('creates a transaction with the correct data', async () => {
255
- salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
257
+ const id = Symbol('recurring-payment-id')
258
+ const agreementId = Symbol('agreement-id')
259
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId, id })])
256
260
 
257
261
  const isLicenceForYou = Symbol('isLicenceForYou')
258
262
  const isRenewal = Symbol('isRenewal')
@@ -277,7 +281,10 @@ describe('recurring-payments-processor', () => {
277
281
 
278
282
  const expectedData = {
279
283
  dataSource: 'Recurring Payment',
280
- agreementId: 'test-agreement-id',
284
+ recurringPayment: {
285
+ agreementId,
286
+ id
287
+ },
281
288
  permissions: [
282
289
  {
283
290
  isLicenceForYou,
@@ -303,6 +310,31 @@ describe('recurring-payments-processor', () => {
303
310
  expect(salesApi.createTransaction).toHaveBeenCalledWith(expectedData)
304
311
  })
305
312
 
313
+ it('creates a payment journal entry', async () => {
314
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
315
+ const samplePayment = {
316
+ payment_id: Symbol('payment-id'),
317
+ created_date: Symbol('created-date')
318
+ }
319
+ const sampleTransaction = {
320
+ id: Symbol('transaction-id'),
321
+ cost: 99
322
+ }
323
+ sendPayment.mockResolvedValueOnce(samplePayment)
324
+ salesApi.createTransaction.mockResolvedValueOnce(sampleTransaction)
325
+
326
+ await processRecurringPayments()
327
+
328
+ expect(salesApi.createPaymentJournal).toHaveBeenCalledWith(
329
+ sampleTransaction.id,
330
+ expect.objectContaining({
331
+ paymentReference: samplePayment.payment_id,
332
+ paymentTimestamp: samplePayment.created_date,
333
+ paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
334
+ })
335
+ )
336
+ })
337
+
306
338
  it('strips the concession name returned by preparePermissionDataForRenewal before passing to createTransaction', async () => {
307
339
  salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
308
340
 
@@ -387,7 +419,7 @@ describe('recurring-payments-processor', () => {
387
419
  const agreementId = Symbol('agreementId')
388
420
  const transactionId = 'transactionId'
389
421
 
390
- salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment('foo', agreementId)])
422
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ referenceNumber: 'foo', agreementId: agreementId })])
391
423
 
392
424
  salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
393
425
  licensee: { countryCode: 'GB-ENG' }
@@ -529,7 +561,7 @@ describe('recurring-payments-processor', () => {
529
561
 
530
562
  it('should call setTimeout with correct delay when there are recurring payments', async () => {
531
563
  const referenceNumber = Symbol('reference')
532
- salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment(referenceNumber)])
564
+ salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment({ referenceNumber })])
533
565
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
534
566
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
535
567
  getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
@@ -592,7 +624,7 @@ describe('recurring-payments-processor', () => {
592
624
  'console error displays "Payment failed. Recurring payment agreement for: %s set to be cancelled" when payment is a %status',
593
625
  async (agreementId, mockStatus, status) => {
594
626
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
595
- salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment('reference', agreementId)])
627
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId })])
596
628
  const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
597
629
  sendPayment.mockResolvedValueOnce(mockPaymentResponse)
598
630
  getPaymentStatus.mockResolvedValueOnce(mockStatus)
@@ -605,6 +637,30 @@ describe('recurring-payments-processor', () => {
605
637
  }
606
638
  )
607
639
 
640
+ it.each([
641
+ ['a failure', 'agreement-id', getPaymentStatusFailure()],
642
+ ['a failure', 'test-agreement-id', getPaymentStatusFailure()],
643
+ ['a failure', 'another-agreement-id', getPaymentStatusFailure()],
644
+ ['an error', 'agreement-id', getPaymentStatusError()],
645
+ ['an error', 'test-agreement-id', getPaymentStatusError()],
646
+ ['an error', 'another-agreement-id', getPaymentStatusError()]
647
+ ])('cancelRecurringPayment is called when payment is %s', async (_status, agreementId, mockStatus) => {
648
+ salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId })])
649
+ const id = Symbol('recurring-payment-id')
650
+ salesApi.createTransaction.mockResolvedValueOnce({
651
+ recurringPayment: {
652
+ id
653
+ }
654
+ })
655
+ const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
656
+ sendPayment.mockResolvedValueOnce(mockPaymentResponse)
657
+ getPaymentStatus.mockResolvedValueOnce(mockStatus)
658
+
659
+ await processRecurringPayments()
660
+
661
+ expect(salesApi.cancelRecurringPayment).toHaveBeenCalledWith(id)
662
+ })
663
+
608
664
  it('updatePaymentJournal is called with transaction id and failed status code payment is not succesful and payment journal exists', async () => {
609
665
  salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
610
666
  const transactionId = 'test-transaction-id'
@@ -646,8 +702,8 @@ describe('recurring-payments-processor', () => {
646
702
  references.push(Symbol('reference' + i))
647
703
  }
648
704
  const mockGetDueRecurringPayments = []
649
- references.forEach(reference => {
650
- mockGetDueRecurringPayments.push(getMockDueRecurringPayment(reference))
705
+ references.forEach(referenceNumber => {
706
+ mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ referenceNumber }))
651
707
  })
652
708
  salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
653
709
  const mockPaymentResponse = { payment_id: 'test-payment-id' }
@@ -667,8 +723,14 @@ describe('recurring-payments-processor', () => {
667
723
 
668
724
  it('creates a transaction for each one', async () => {
669
725
  const mockGetDueRecurringPayments = []
726
+ const agreementIds = []
727
+ const ids = []
670
728
  for (let i = 0; i < count; i++) {
671
- mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i))
729
+ const agreementId = Symbol(`agreement-id-${i}`)
730
+ const id = Symbol(`recurring-payment-${i}`)
731
+ agreementIds.push(agreementId)
732
+ ids.push(id)
733
+ mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId, id, referenceNumber: i }))
672
734
  }
673
735
  salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
674
736
 
@@ -685,11 +747,14 @@ describe('recurring-payments-processor', () => {
685
747
  })
686
748
 
687
749
  const expectedData = []
688
- permits.forEach(permit => {
750
+ permits.forEach((permit, i) => {
689
751
  expectedData.push([
690
752
  {
691
753
  dataSource: 'Recurring Payment',
692
- agreementId: 'test-agreement-id',
754
+ recurringPayment: {
755
+ agreementId: agreementIds[i],
756
+ id: ids[i]
757
+ },
693
758
  permissions: [expect.objectContaining({ permitId: permit })]
694
759
  }
695
760
  ])
@@ -706,7 +771,7 @@ describe('recurring-payments-processor', () => {
706
771
  for (let i = 0; i < count; i++) {
707
772
  const agreementId = Symbol(`agreementId${1}`)
708
773
  agreementIds.push(agreementId)
709
- mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i, agreementId))
774
+ mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId }))
710
775
  }
711
776
  salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
712
777
 
@@ -749,7 +814,7 @@ describe('recurring-payments-processor', () => {
749
814
  for (let i = 0; i < count; i++) {
750
815
  const agreementId = Symbol(`agreementId${1}`)
751
816
  agreementIds.push(agreementId)
752
- mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i, agreementId))
817
+ mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId }))
753
818
  }
754
819
  salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
755
820
 
@@ -1,6 +1,13 @@
1
1
  'use strict'
2
2
  import recurringPaymentsJob from 'commander'
3
3
  import { processRecurringPayments } from './recurring-payments-processor.js'
4
+ import path from 'path'
5
+ import fs from 'fs'
6
+
7
+ const pkgPath = path.join(process.cwd(), 'package.json')
8
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
9
+
10
+ console.log('Recurring payments job starting at %s. name: %s. version: %s', new Date().toISOString(), pkg.name, pkg.version)
4
11
 
5
12
  const delay = parseInt(process.env.RECURRING_PAYMENTS_LOCAL_DELAY || '0', 10)
6
13
  if (delay > 0) {
@@ -64,19 +64,25 @@ const requestPayments = async dueRCPayments => {
64
64
 
65
65
  const processRecurringPayment = async record => {
66
66
  const referenceNumber = record.expanded.activePermission.entity.referenceNumber
67
- const agreementId = record.entity.agreementId
68
- const transaction = await createNewTransaction(referenceNumber, agreementId)
67
+ const { agreementId, id } = record.entity
68
+ const transaction = await createNewTransaction(referenceNumber, { agreementId, id })
69
69
  return takeRecurringPayment(agreementId, transaction)
70
70
  }
71
71
 
72
- const createNewTransaction = async (referenceNumber, agreementId) => {
73
- const transactionData = await processPermissionData(referenceNumber, agreementId)
72
+ const createNewTransaction = async (referenceNumber, recurringPayment) => {
73
+ const transactionData = await processPermissionData(referenceNumber, recurringPayment)
74
74
  return salesApi.createTransaction(transactionData)
75
75
  }
76
76
 
77
77
  const takeRecurringPayment = async (agreementId, transaction) => {
78
78
  const preparedPayment = preparePayment(agreementId, transaction)
79
79
  const payment = await sendPayment(preparedPayment)
80
+ await salesApi.createPaymentJournal(transaction.id, {
81
+ paymentReference: payment.payment_id,
82
+ paymentTimestamp: payment.created_date,
83
+ paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
84
+ })
85
+
80
86
  return {
81
87
  agreementId,
82
88
  paymentId: payment.payment_id,
@@ -85,12 +91,12 @@ const takeRecurringPayment = async (agreementId, transaction) => {
85
91
  }
86
92
  }
87
93
 
88
- const processPermissionData = async (referenceNumber, agreementId) => {
94
+ const processPermissionData = async (referenceNumber, recurringPayment) => {
89
95
  const data = await salesApi.preparePermissionDataForRenewal(referenceNumber)
90
96
  const licenseeWithoutCountryCode = Object.assign((({ countryCode: _countryCode, ...l }) => l)(data.licensee))
91
97
  return {
92
98
  dataSource: 'Recurring Payment',
93
- agreementId,
99
+ recurringPayment,
94
100
  permissions: [
95
101
  {
96
102
  isLicenceForYou: data.isLicenceForYou,
@@ -148,6 +154,7 @@ const processRecurringPaymentStatus = async payment => {
148
154
  paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.Failed
149
155
  })
150
156
  }
157
+ await salesApi.cancelRecurringPayment(payment.transaction.recurringPayment.id)
151
158
  }
152
159
  } catch (error) {
153
160
  const status = error.response?.status
@@ -159,5 +166,6 @@ const processRecurringPaymentStatus = async payment => {
159
166
  } else {
160
167
  debug(`Unexpected error fetching payment status for ${payment.paymentId}.`)
161
168
  }
169
+ await salesApi.cancelRecurringPayment(payment.transaction.recurringPayment.id)
162
170
  }
163
171
  }
@@ -16,9 +16,11 @@ describe('govuk-pay-service', () => {
16
16
 
17
17
  it('sendPayment should return response from createPayment in json format', async () => {
18
18
  const mockPreparedPayment = { id: 'test-payment-id' }
19
- const mockResponse = { status: 'success', paymentId: 'abc123' }
19
+ const mockResponse = { state: { status: 'created' }, payment_id: 'abcde12345' }
20
20
 
21
21
  const mockFetchResponse = {
22
+ status: 200,
23
+ ok: true,
22
24
  json: jest.fn().mockResolvedValue(mockResponse)
23
25
  }
24
26
  govUkPayApi.createPayment.mockResolvedValue(mockFetchResponse)
@@ -67,6 +69,66 @@ describe('govuk-pay-service', () => {
67
69
  expect(consoleSpy).toHaveBeenCalledWith('Error creating payment', preparedPayment.id)
68
70
  }
69
71
  })
72
+
73
+ it('should throw an error when response is not ok', async () => {
74
+ const mockFetchResponse = {
75
+ ok: false,
76
+ status: 400,
77
+ json: jest.fn().mockResolvedValue({
78
+ code: 'P0102',
79
+ field: 'agreement_id',
80
+ description: 'Invalid attribute value: agreement_id. Agreement does not exist'
81
+ })
82
+ }
83
+ govUkPayApi.createPayment.mockResolvedValueOnce(mockFetchResponse)
84
+
85
+ await expect(
86
+ sendPayment({
87
+ amount: 100,
88
+ description: 'The recurring card payment for your rod fishing licence',
89
+ id: 'a50f0d51-295f-42b3-98f8-97c0641ede5a',
90
+ authorisation_mode: 'agreement',
91
+ agreement_id: 'does_not_exist'
92
+ })
93
+ ).rejects.toThrow('Unexpected response from GOV.UK Pay API')
94
+ })
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
+ })
70
132
  })
71
133
 
72
134
  describe('getPaymentStatus', () => {
@@ -81,7 +143,7 @@ describe('govuk-pay-service', () => {
81
143
  })
82
144
 
83
145
  it('should return the payment status on successful response', async () => {
84
- const mockPaymentStatus = { code: 'P1234', description: 'Success' }
146
+ const mockPaymentStatus = { amount: 37.5, state: { status: 'success', finished: 'true' } }
85
147
  govUkPayApi.fetchPaymentStatus.mockResolvedValue({
86
148
  ok: true,
87
149
  json: jest.fn().mockResolvedValue(mockPaymentStatus)
@@ -96,25 +158,47 @@ describe('govuk-pay-service', () => {
96
158
  })
97
159
 
98
160
  it('should throw an error when response is not ok', async () => {
99
- const mockErrorDetails = { error: 'Payment not found' }
100
161
  const mockFetchResponse = {
101
162
  ok: false,
102
- json: jest.fn().mockResolvedValue(mockErrorDetails)
163
+ status: 404,
164
+ json: jest.fn().mockResolvedValue({
165
+ code: 'P0200',
166
+ field: 'payment_id',
167
+ description: 'No payment matched the payment id you provided'
168
+ })
103
169
  }
104
170
  govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockFetchResponse)
105
171
 
106
- await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Payment not found')
172
+ await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Unexpected response from GOV.UK Pay API')
107
173
  })
108
174
 
109
- it('should throw an error when response is not ok but errorDetails has no value', async () => {
110
- const mockErrorDetails = {}
175
+ it('should log details when response is not ok', async () => {
176
+ const serviceResponseBody = {
177
+ code: 'P0200',
178
+ field: 'payment_id',
179
+ description: 'No payment matched the payment id you provided'
180
+ }
111
181
  const mockFetchResponse = {
112
182
  ok: false,
113
- json: jest.fn().mockResolvedValue(mockErrorDetails)
183
+ status: 404,
184
+ json: jest.fn().mockResolvedValue(serviceResponseBody)
114
185
  }
115
186
  govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockFetchResponse)
187
+ jest.spyOn(console, 'error')
188
+ const paymentId = 'invalid-payment-id'
116
189
 
117
- await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Error fetching payment status')
190
+ try {
191
+ await getPaymentStatus(paymentId)
192
+ } catch {}
193
+
194
+ expect(console.error).toHaveBeenCalledWith(
195
+ expect.objectContaining({
196
+ method: 'GET',
197
+ status: mockFetchResponse.status,
198
+ response: serviceResponseBody,
199
+ paymentId
200
+ })
201
+ )
118
202
  })
119
203
 
120
204
  it('should throw an error when fetchPaymentStatus fails', async () => {
@@ -3,13 +3,26 @@ 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
- try {
7
- const response = await govUkPayApi.createPayment(preparedPayment, true)
8
- return await response.json()
9
- } catch (e) {
10
- console.error('Error creating payment', preparedPayment.id)
11
- throw e
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
+ }
14
+ const response = await createPayment()
15
+ 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')
12
24
  }
25
+ return response.json()
13
26
  }
14
27
 
15
28
  export const getPaymentStatus = async paymentId => {
@@ -17,21 +30,28 @@ export const getPaymentStatus = async paymentId => {
17
30
  throw new Error('Invalid payment ID')
18
31
  }
19
32
 
20
- try {
21
- const response = await govUkPayApi.fetchPaymentStatus(paymentId, true)
22
-
23
- if (!response.ok) {
24
- const errorDetails = await response.json()
25
- console.log(errorDetails)
26
- throw new Error(errorDetails.error || 'Error fetching payment status')
33
+ const fetchPaymentStatus = async () => {
34
+ try {
35
+ return await govUkPayApi.fetchPaymentStatus(paymentId, true)
36
+ } catch (e) {
37
+ console.error('Error fetching payment status', paymentId)
38
+ throw e
27
39
  }
40
+ }
41
+
42
+ const response = await fetchPaymentStatus()
28
43
 
29
- const paymentStatus = await response.json()
30
- return paymentStatus
31
- } catch (error) {
32
- console.error('Error in getPaymentStatus:', error)
33
- throw error
44
+ if (!response.ok) {
45
+ console.error({
46
+ method: 'GET',
47
+ status: response.status,
48
+ response: await response.json(),
49
+ paymentId
50
+ })
51
+ throw new Error('Unexpected response from GOV.UK Pay API')
34
52
  }
53
+
54
+ return response.json()
35
55
  }
36
56
 
37
57
  export const isGovPayUp = async () => {