@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 +4 -4
- package/src/__tests__/recurring-payments-job.spec.js +19 -0
- package/src/__tests__/recurring-payments-processor.spec.js +93 -28
- package/src/recurring-payments-job.js +7 -0
- package/src/recurring-payments-processor.js +14 -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.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.
|
|
40
|
-
"@defra-fish/connectors-lib": "1.63.0-rc.
|
|
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": "
|
|
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
|
-
|
|
24
|
-
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
650
|
-
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
@@ -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: '
|
|
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 () => {
|