@defra-fish/recurring-payments-job 1.63.0-rc.0 → 1.63.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/package.json +4 -4
- package/src/__tests__/recurring-payments-job.spec.js +19 -0
- package/src/__tests__/recurring-payments-processor.spec.js +68 -28
- package/src/recurring-payments-job.js +7 -0
- package/src/recurring-payments-processor.js +12 -6
- package/src/services/__tests__/govuk-pay-service.spec.js +93 -9
- package/src/services/govuk-pay-service.js +38 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/recurring-payments-job",
|
|
3
|
-
"version": "1.63.0-rc.
|
|
3
|
+
"version": "1.63.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.63.0-rc.
|
|
40
|
-
"@defra-fish/connectors-lib": "1.63.0-rc.
|
|
39
|
+
"@defra-fish/business-rules-lib": "1.63.0-rc.10",
|
|
40
|
+
"@defra-fish/connectors-lib": "1.63.0-rc.10",
|
|
41
41
|
"commander": "^7.2.0",
|
|
42
42
|
"debug": "^4.3.3",
|
|
43
43
|
"moment-timezone": "^0.5.34"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "c4301461b148cbf244c1bb5c834521243ec0a534"
|
|
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,18 @@ jest.mock('@defra-fish/business-rules-lib', () => ({
|
|
|
20
20
|
}))
|
|
21
21
|
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
22
22
|
salesApi: {
|
|
23
|
-
|
|
24
|
-
preparePermissionDataForRenewal: jest.fn(() => ({
|
|
25
|
-
licensee: { countryCode: 'GB-ENG' }
|
|
26
|
-
})),
|
|
23
|
+
createPaymentJournal: jest.fn(),
|
|
27
24
|
createTransaction: jest.fn(() => ({
|
|
28
25
|
id: 'test-transaction-id',
|
|
29
26
|
cost: 30
|
|
30
27
|
})),
|
|
28
|
+
getDueRecurringPayments: jest.fn(() => []),
|
|
29
|
+
getPaymentJournal: jest.fn(),
|
|
30
|
+
preparePermissionDataForRenewal: jest.fn(() => ({
|
|
31
|
+
licensee: { countryCode: 'GB-ENG' }
|
|
32
|
+
})),
|
|
31
33
|
processRPResult: jest.fn(),
|
|
32
|
-
updatePaymentJournal: jest.fn()
|
|
33
|
-
getPaymentJournal: jest.fn()
|
|
34
|
+
updatePaymentJournal: jest.fn()
|
|
34
35
|
}
|
|
35
36
|
}))
|
|
36
37
|
|
|
@@ -59,8 +60,8 @@ const getMockPaymentRequestResponse = () => [
|
|
|
59
60
|
}
|
|
60
61
|
]
|
|
61
62
|
|
|
62
|
-
const getMockDueRecurringPayment = (
|
|
63
|
-
entity: { agreementId },
|
|
63
|
+
const getMockDueRecurringPayment = ({ agreementId = 'test-agreement-id', id = 'abc-123', referenceNumber = '123' } = {}) => ({
|
|
64
|
+
entity: { id, agreementId },
|
|
64
65
|
expanded: { activePermission: { entity: { referenceNumber } } }
|
|
65
66
|
})
|
|
66
67
|
|
|
@@ -147,10 +148,10 @@ describe('recurring-payments-processor', () => {
|
|
|
147
148
|
it('prepares and sends all payment requests, even if some fail', async () => {
|
|
148
149
|
const agreementIds = [Symbol('agreementId1'), Symbol('agreementId2'), Symbol('agreementId3'), Symbol('agreementId4')]
|
|
149
150
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
150
|
-
getMockDueRecurringPayment('fee', agreementIds[0]),
|
|
151
|
-
getMockDueRecurringPayment('fi', agreementIds[1]),
|
|
152
|
-
getMockDueRecurringPayment('foe', agreementIds[2]),
|
|
153
|
-
getMockDueRecurringPayment('fum', agreementIds[3])
|
|
151
|
+
getMockDueRecurringPayment({ referenceNumber: 'fee', agreementId: agreementIds[0] }),
|
|
152
|
+
getMockDueRecurringPayment({ referenceNumber: 'fi', agreementId: agreementIds[1] }),
|
|
153
|
+
getMockDueRecurringPayment({ referenceNumber: 'foe', agreementId: agreementIds[2] }),
|
|
154
|
+
getMockDueRecurringPayment({ referenceNumber: 'fum', agreementId: agreementIds[3] })
|
|
154
155
|
])
|
|
155
156
|
|
|
156
157
|
const permissionData = { licensee: { countryCode: 'GB-ENG' } }
|
|
@@ -202,9 +203,9 @@ describe('recurring-payments-processor', () => {
|
|
|
202
203
|
it('logs an error for every failure', async () => {
|
|
203
204
|
const errors = [new Error('error 1'), new Error('error 2'), new Error('error 3')]
|
|
204
205
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
205
|
-
getMockDueRecurringPayment('fee', 'a1'),
|
|
206
|
-
getMockDueRecurringPayment('fi', 'a2'),
|
|
207
|
-
getMockDueRecurringPayment('foe', 'a3')
|
|
206
|
+
getMockDueRecurringPayment({ referenceNumber: 'fee', agreementId: 'a1' }),
|
|
207
|
+
getMockDueRecurringPayment({ referenceNumber: 'fi', agreementId: 'a2' }),
|
|
208
|
+
getMockDueRecurringPayment({ referenceNumber: 'foe', agreementId: 'a3' })
|
|
208
209
|
])
|
|
209
210
|
const permissionData = { licensee: { countryCode: 'GB-ENG' } }
|
|
210
211
|
salesApi.preparePermissionDataForRenewal
|
|
@@ -241,7 +242,7 @@ describe('recurring-payments-processor', () => {
|
|
|
241
242
|
|
|
242
243
|
it('prepares the data for found recurring payments', async () => {
|
|
243
244
|
const referenceNumber = Symbol('reference')
|
|
244
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment(referenceNumber)])
|
|
245
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ referenceNumber })])
|
|
245
246
|
const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
|
|
246
247
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
247
248
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
@@ -252,7 +253,9 @@ describe('recurring-payments-processor', () => {
|
|
|
252
253
|
})
|
|
253
254
|
|
|
254
255
|
it('creates a transaction with the correct data', async () => {
|
|
255
|
-
|
|
256
|
+
const id = Symbol('recurring-payment-id')
|
|
257
|
+
const agreementId = Symbol('agreement-id')
|
|
258
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId, id })])
|
|
256
259
|
|
|
257
260
|
const isLicenceForYou = Symbol('isLicenceForYou')
|
|
258
261
|
const isRenewal = Symbol('isRenewal')
|
|
@@ -277,7 +280,10 @@ describe('recurring-payments-processor', () => {
|
|
|
277
280
|
|
|
278
281
|
const expectedData = {
|
|
279
282
|
dataSource: 'Recurring Payment',
|
|
280
|
-
|
|
283
|
+
recurringPayment: {
|
|
284
|
+
agreementId,
|
|
285
|
+
id
|
|
286
|
+
},
|
|
281
287
|
permissions: [
|
|
282
288
|
{
|
|
283
289
|
isLicenceForYou,
|
|
@@ -303,6 +309,31 @@ describe('recurring-payments-processor', () => {
|
|
|
303
309
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(expectedData)
|
|
304
310
|
})
|
|
305
311
|
|
|
312
|
+
it('creates a payment journal entry', async () => {
|
|
313
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
314
|
+
const samplePayment = {
|
|
315
|
+
payment_id: Symbol('payment-id'),
|
|
316
|
+
created_date: Symbol('created-date')
|
|
317
|
+
}
|
|
318
|
+
const sampleTransaction = {
|
|
319
|
+
id: Symbol('transaction-id'),
|
|
320
|
+
cost: 99
|
|
321
|
+
}
|
|
322
|
+
sendPayment.mockResolvedValueOnce(samplePayment)
|
|
323
|
+
salesApi.createTransaction.mockResolvedValueOnce(sampleTransaction)
|
|
324
|
+
|
|
325
|
+
await processRecurringPayments()
|
|
326
|
+
|
|
327
|
+
expect(salesApi.createPaymentJournal).toHaveBeenCalledWith(
|
|
328
|
+
sampleTransaction.id,
|
|
329
|
+
expect.objectContaining({
|
|
330
|
+
paymentReference: samplePayment.payment_id,
|
|
331
|
+
paymentTimestamp: samplePayment.created_date,
|
|
332
|
+
paymentStatus: PAYMENT_JOURNAL_STATUS_CODES.InProgress
|
|
333
|
+
})
|
|
334
|
+
)
|
|
335
|
+
})
|
|
336
|
+
|
|
306
337
|
it('strips the concession name returned by preparePermissionDataForRenewal before passing to createTransaction', async () => {
|
|
307
338
|
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
308
339
|
|
|
@@ -387,7 +418,7 @@ describe('recurring-payments-processor', () => {
|
|
|
387
418
|
const agreementId = Symbol('agreementId')
|
|
388
419
|
const transactionId = 'transactionId'
|
|
389
420
|
|
|
390
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment('foo', agreementId)])
|
|
421
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ referenceNumber: 'foo', agreementId: agreementId })])
|
|
391
422
|
|
|
392
423
|
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
393
424
|
licensee: { countryCode: 'GB-ENG' }
|
|
@@ -529,7 +560,7 @@ describe('recurring-payments-processor', () => {
|
|
|
529
560
|
|
|
530
561
|
it('should call setTimeout with correct delay when there are recurring payments', async () => {
|
|
531
562
|
const referenceNumber = Symbol('reference')
|
|
532
|
-
salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment(referenceNumber)])
|
|
563
|
+
salesApi.getDueRecurringPayments.mockResolvedValueOnce([getMockDueRecurringPayment({ referenceNumber })])
|
|
533
564
|
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
534
565
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
535
566
|
getPaymentStatus.mockResolvedValueOnce(getPaymentStatusSuccess())
|
|
@@ -592,7 +623,7 @@ describe('recurring-payments-processor', () => {
|
|
|
592
623
|
'console error displays "Payment failed. Recurring payment agreement for: %s set to be cancelled" when payment is a %status',
|
|
593
624
|
async (agreementId, mockStatus, status) => {
|
|
594
625
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
595
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment(
|
|
626
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment({ agreementId })])
|
|
596
627
|
const mockPaymentResponse = { payment_id: 'test-payment-id', created_date: '2025-01-01T00:00:00.000Z' }
|
|
597
628
|
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
598
629
|
getPaymentStatus.mockResolvedValueOnce(mockStatus)
|
|
@@ -646,8 +677,8 @@ describe('recurring-payments-processor', () => {
|
|
|
646
677
|
references.push(Symbol('reference' + i))
|
|
647
678
|
}
|
|
648
679
|
const mockGetDueRecurringPayments = []
|
|
649
|
-
references.forEach(
|
|
650
|
-
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(
|
|
680
|
+
references.forEach(referenceNumber => {
|
|
681
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ referenceNumber }))
|
|
651
682
|
})
|
|
652
683
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
653
684
|
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
@@ -667,8 +698,14 @@ describe('recurring-payments-processor', () => {
|
|
|
667
698
|
|
|
668
699
|
it('creates a transaction for each one', async () => {
|
|
669
700
|
const mockGetDueRecurringPayments = []
|
|
701
|
+
const agreementIds = []
|
|
702
|
+
const ids = []
|
|
670
703
|
for (let i = 0; i < count; i++) {
|
|
671
|
-
|
|
704
|
+
const agreementId = Symbol(`agreement-id-${i}`)
|
|
705
|
+
const id = Symbol(`recurring-payment-${i}`)
|
|
706
|
+
agreementIds.push(agreementId)
|
|
707
|
+
ids.push(id)
|
|
708
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId, id, referenceNumber: i }))
|
|
672
709
|
}
|
|
673
710
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
674
711
|
|
|
@@ -685,11 +722,14 @@ describe('recurring-payments-processor', () => {
|
|
|
685
722
|
})
|
|
686
723
|
|
|
687
724
|
const expectedData = []
|
|
688
|
-
permits.forEach(permit => {
|
|
725
|
+
permits.forEach((permit, i) => {
|
|
689
726
|
expectedData.push([
|
|
690
727
|
{
|
|
691
728
|
dataSource: 'Recurring Payment',
|
|
692
|
-
|
|
729
|
+
recurringPayment: {
|
|
730
|
+
agreementId: agreementIds[i],
|
|
731
|
+
id: ids[i]
|
|
732
|
+
},
|
|
693
733
|
permissions: [expect.objectContaining({ permitId: permit })]
|
|
694
734
|
}
|
|
695
735
|
])
|
|
@@ -706,7 +746,7 @@ describe('recurring-payments-processor', () => {
|
|
|
706
746
|
for (let i = 0; i < count; i++) {
|
|
707
747
|
const agreementId = Symbol(`agreementId${1}`)
|
|
708
748
|
agreementIds.push(agreementId)
|
|
709
|
-
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(
|
|
749
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId }))
|
|
710
750
|
}
|
|
711
751
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
712
752
|
|
|
@@ -749,7 +789,7 @@ describe('recurring-payments-processor', () => {
|
|
|
749
789
|
for (let i = 0; i < count; i++) {
|
|
750
790
|
const agreementId = Symbol(`agreementId${1}`)
|
|
751
791
|
agreementIds.push(agreementId)
|
|
752
|
-
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(
|
|
792
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment({ agreementId }))
|
|
753
793
|
}
|
|
754
794
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
755
795
|
|
|
@@ -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
|
|
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,
|
|
73
|
-
const transactionData = await processPermissionData(referenceNumber,
|
|
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,
|
|
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
|
-
|
|
99
|
+
recurringPayment,
|
|
94
100
|
permissions: [
|
|
95
101
|
{
|
|
96
102
|
isLicenceForYou: data.isLicenceForYou,
|
|
@@ -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: '
|
|
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 = {
|
|
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
|
-
|
|
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('
|
|
172
|
+
await expect(getPaymentStatus('invalid-payment-id')).rejects.toThrow('Unexpected response from GOV.UK Pay API')
|
|
107
173
|
})
|
|
108
174
|
|
|
109
|
-
it('should
|
|
110
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 () => {
|