@defra-fish/recurring-payments-job 1.58.0-rc.8 → 1.58.0
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.58.0
|
|
3
|
+
"version": "1.58.0",
|
|
4
4
|
"description": "Rod Licensing Recurring Payments Job",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@defra-fish/business-rules-lib": "1.58.0
|
|
40
|
-
"@defra-fish/connectors-lib": "1.58.0
|
|
39
|
+
"@defra-fish/business-rules-lib": "1.58.0",
|
|
40
|
+
"@defra-fish/connectors-lib": "1.58.0",
|
|
41
41
|
"commander": "^7.2.0",
|
|
42
42
|
"moment-timezone": "^0.5.34"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "4d2c2ef1bf3016fa38140a1bd78bf26ba5adb68d"
|
|
45
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { salesApi } from '@defra-fish/connectors-lib'
|
|
2
2
|
import { processRecurringPayments } from '../recurring-payments-processor.js'
|
|
3
|
+
import { sendPayment } from '../services/govuk-pay-service.js'
|
|
3
4
|
|
|
4
5
|
jest.mock('@defra-fish/business-rules-lib')
|
|
5
6
|
jest.mock('@defra-fish/connectors-lib', () => ({
|
|
@@ -8,9 +9,14 @@ jest.mock('@defra-fish/connectors-lib', () => ({
|
|
|
8
9
|
preparePermissionDataForRenewal: jest.fn(() => ({
|
|
9
10
|
licensee: { countryCode: 'GB-ENG' }
|
|
10
11
|
})),
|
|
11
|
-
createTransaction: jest.fn()
|
|
12
|
+
createTransaction: jest.fn(() => ({
|
|
13
|
+
cost: 30
|
|
14
|
+
}))
|
|
12
15
|
}
|
|
13
16
|
}))
|
|
17
|
+
jest.mock('../services/govuk-pay-service.js', () => ({
|
|
18
|
+
sendPayment: jest.fn()
|
|
19
|
+
}))
|
|
14
20
|
|
|
15
21
|
describe('recurring-payments-processor', () => {
|
|
16
22
|
beforeEach(() => {
|
|
@@ -53,7 +59,9 @@ describe('recurring-payments-processor', () => {
|
|
|
53
59
|
|
|
54
60
|
it('prepares the data for found recurring payments', async () => {
|
|
55
61
|
const referenceNumber = Symbol('reference')
|
|
56
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
62
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment(referenceNumber)])
|
|
63
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
64
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
57
65
|
|
|
58
66
|
await processRecurringPayments()
|
|
59
67
|
|
|
@@ -61,7 +69,7 @@ describe('recurring-payments-processor', () => {
|
|
|
61
69
|
})
|
|
62
70
|
|
|
63
71
|
it('creates a transaction with the correct data', async () => {
|
|
64
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
72
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
65
73
|
|
|
66
74
|
const isLicenceForYou = Symbol('isLicenceForYou')
|
|
67
75
|
const isRenewal = Symbol('isRenewal')
|
|
@@ -102,13 +110,16 @@ describe('recurring-payments-processor', () => {
|
|
|
102
110
|
]
|
|
103
111
|
}
|
|
104
112
|
|
|
113
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
114
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
115
|
+
|
|
105
116
|
await processRecurringPayments()
|
|
106
117
|
|
|
107
118
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(expectedData)
|
|
108
119
|
})
|
|
109
120
|
|
|
110
121
|
it('strips the concession name returned by preparePermissionDataForRenewal before passing to createTransaction', async () => {
|
|
111
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
122
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
112
123
|
|
|
113
124
|
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
114
125
|
licensee: {
|
|
@@ -123,6 +134,9 @@ describe('recurring-payments-processor', () => {
|
|
|
123
134
|
]
|
|
124
135
|
})
|
|
125
136
|
|
|
137
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
138
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
139
|
+
|
|
126
140
|
await processRecurringPayments()
|
|
127
141
|
|
|
128
142
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
@@ -141,7 +155,7 @@ describe('recurring-payments-processor', () => {
|
|
|
141
155
|
})
|
|
142
156
|
|
|
143
157
|
it('assigns the correct startDate when licenceStartTime is present', async () => {
|
|
144
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
158
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
145
159
|
|
|
146
160
|
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
147
161
|
licensee: { countryCode: 'GB-ENG' },
|
|
@@ -149,6 +163,9 @@ describe('recurring-payments-processor', () => {
|
|
|
149
163
|
licenceStartTime: 15
|
|
150
164
|
})
|
|
151
165
|
|
|
166
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
167
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
168
|
+
|
|
152
169
|
await processRecurringPayments()
|
|
153
170
|
|
|
154
171
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
@@ -159,13 +176,16 @@ describe('recurring-payments-processor', () => {
|
|
|
159
176
|
})
|
|
160
177
|
|
|
161
178
|
it('assigns the correct startDate when licenceStartTime is not present', async () => {
|
|
162
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
179
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
163
180
|
|
|
164
181
|
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
165
182
|
licensee: { countryCode: 'GB-ENG' },
|
|
166
183
|
licenceStartDate: '2020-03-14'
|
|
167
184
|
})
|
|
168
185
|
|
|
186
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
187
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
188
|
+
|
|
169
189
|
await processRecurringPayments()
|
|
170
190
|
|
|
171
191
|
expect(salesApi.createTransaction).toHaveBeenCalledWith(
|
|
@@ -176,7 +196,7 @@ describe('recurring-payments-processor', () => {
|
|
|
176
196
|
})
|
|
177
197
|
|
|
178
198
|
it('raises an error if createTransaction fails', async () => {
|
|
179
|
-
salesApi.getDueRecurringPayments.mockReturnValueOnce([
|
|
199
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment()])
|
|
180
200
|
const error = 'Wuh-oh!'
|
|
181
201
|
salesApi.createTransaction.mockImplementationOnce(() => {
|
|
182
202
|
throw new Error(error)
|
|
@@ -185,6 +205,37 @@ describe('recurring-payments-processor', () => {
|
|
|
185
205
|
await expect(processRecurringPayments()).rejects.toThrowError(error)
|
|
186
206
|
})
|
|
187
207
|
|
|
208
|
+
it('prepares and sends the payment request', async () => {
|
|
209
|
+
const agreementId = Symbol('agreementId')
|
|
210
|
+
const transactionId = Symbol('transactionId')
|
|
211
|
+
|
|
212
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce([getMockDueRecurringPayment('foo', agreementId)])
|
|
213
|
+
|
|
214
|
+
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
215
|
+
licensee: { countryCode: 'GB-ENG' }
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
salesApi.createTransaction.mockReturnValueOnce({
|
|
219
|
+
cost: 50,
|
|
220
|
+
id: transactionId
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
224
|
+
sendPayment.mockResolvedValueOnce(mockPaymentResponse)
|
|
225
|
+
|
|
226
|
+
const expectedData = {
|
|
227
|
+
amount: 5000,
|
|
228
|
+
description: 'The recurring card payment for your rod fishing licence',
|
|
229
|
+
reference: transactionId,
|
|
230
|
+
authorisation_mode: 'agreement',
|
|
231
|
+
agreement_id: agreementId
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await processRecurringPayments()
|
|
235
|
+
|
|
236
|
+
expect(sendPayment).toHaveBeenCalledWith(expectedData)
|
|
237
|
+
})
|
|
238
|
+
|
|
188
239
|
describe.each([2, 3, 10])('if there are %d recurring payments', count => {
|
|
189
240
|
it('prepares the data for each one', async () => {
|
|
190
241
|
const references = []
|
|
@@ -194,9 +245,11 @@ describe('recurring-payments-processor', () => {
|
|
|
194
245
|
|
|
195
246
|
const mockGetDueRecurringPayments = []
|
|
196
247
|
references.forEach(reference => {
|
|
197
|
-
mockGetDueRecurringPayments.push(
|
|
248
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(reference))
|
|
198
249
|
})
|
|
199
250
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
251
|
+
const mockPaymentResponse = { payment_id: 'test-payment-id' }
|
|
252
|
+
sendPayment.mockResolvedValue(mockPaymentResponse)
|
|
200
253
|
|
|
201
254
|
const expectedData = []
|
|
202
255
|
references.forEach(reference => {
|
|
@@ -211,7 +264,7 @@ describe('recurring-payments-processor', () => {
|
|
|
211
264
|
it('creates a transaction for each one', async () => {
|
|
212
265
|
const mockGetDueRecurringPayments = []
|
|
213
266
|
for (let i = 0; i < count; i++) {
|
|
214
|
-
mockGetDueRecurringPayments.push(
|
|
267
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i))
|
|
215
268
|
}
|
|
216
269
|
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
217
270
|
|
|
@@ -241,5 +294,53 @@ describe('recurring-payments-processor', () => {
|
|
|
241
294
|
|
|
242
295
|
expect(salesApi.createTransaction.mock.calls).toEqual(expectedData)
|
|
243
296
|
})
|
|
297
|
+
|
|
298
|
+
it('sends a payment for each one', async () => {
|
|
299
|
+
const mockGetDueRecurringPayments = []
|
|
300
|
+
const agreementIds = []
|
|
301
|
+
for (let i = 0; i < count; i++) {
|
|
302
|
+
const agreementId = Symbol(`agreementId${1}`)
|
|
303
|
+
agreementIds.push(agreementId)
|
|
304
|
+
mockGetDueRecurringPayments.push(getMockDueRecurringPayment(i, agreementId))
|
|
305
|
+
}
|
|
306
|
+
salesApi.getDueRecurringPayments.mockReturnValueOnce(mockGetDueRecurringPayments)
|
|
307
|
+
|
|
308
|
+
const permits = []
|
|
309
|
+
for (let i = 0; i < count; i++) {
|
|
310
|
+
permits.push(Symbol(`permit${i}`))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
permits.forEach((permit, i) => {
|
|
314
|
+
salesApi.preparePermissionDataForRenewal.mockReturnValueOnce({
|
|
315
|
+
licensee: { countryCode: 'GB-ENG' }
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
salesApi.createTransaction.mockReturnValueOnce({
|
|
319
|
+
cost: i,
|
|
320
|
+
id: permit
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const expectedData = []
|
|
325
|
+
permits.forEach((permit, i) => {
|
|
326
|
+
expectedData.push([
|
|
327
|
+
{
|
|
328
|
+
amount: i * 100,
|
|
329
|
+
description: 'The recurring card payment for your rod fishing licence',
|
|
330
|
+
reference: permit,
|
|
331
|
+
authorisation_mode: 'agreement',
|
|
332
|
+
agreement_id: agreementIds[i]
|
|
333
|
+
}
|
|
334
|
+
])
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
await processRecurringPayments()
|
|
338
|
+
expect(sendPayment.mock.calls).toEqual(expectedData)
|
|
339
|
+
})
|
|
244
340
|
})
|
|
245
341
|
})
|
|
342
|
+
|
|
343
|
+
const getMockDueRecurringPayment = (referenceNumber = '123', agreementId = '456') => ({
|
|
344
|
+
entity: { agreementId },
|
|
345
|
+
expanded: { activePermission: { entity: { referenceNumber } } }
|
|
346
|
+
})
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import moment from 'moment-timezone'
|
|
2
2
|
import { SERVICE_LOCAL_TIME } from '@defra-fish/business-rules-lib'
|
|
3
3
|
import { salesApi } from '@defra-fish/connectors-lib'
|
|
4
|
+
import { sendPayment } from './services/govuk-pay-service.js'
|
|
5
|
+
|
|
6
|
+
const payments = []
|
|
4
7
|
|
|
5
8
|
export const processRecurringPayments = async () => {
|
|
6
9
|
if (process.env.RUN_RECURRING_PAYMENTS?.toLowerCase() === 'true') {
|
|
@@ -9,6 +12,7 @@ export const processRecurringPayments = async () => {
|
|
|
9
12
|
const response = await salesApi.getDueRecurringPayments(date)
|
|
10
13
|
console.log('Recurring Payments found: ', response)
|
|
11
14
|
await Promise.all(response.map(record => processRecurringPayment(record)))
|
|
15
|
+
console.log('Recurring Payments processed:', payments)
|
|
12
16
|
} else {
|
|
13
17
|
console.log('Recurring Payments job disabled')
|
|
14
18
|
}
|
|
@@ -16,17 +20,34 @@ export const processRecurringPayments = async () => {
|
|
|
16
20
|
|
|
17
21
|
const processRecurringPayment = async record => {
|
|
18
22
|
const referenceNumber = record.expanded.activePermission.entity.referenceNumber
|
|
23
|
+
const agreementId = record.entity.agreementId
|
|
24
|
+
const transaction = await createNewTransaction(referenceNumber)
|
|
25
|
+
await takeRecurringPayment(agreementId, transaction)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createNewTransaction = async referenceNumber => {
|
|
19
29
|
const transactionData = await processPermissionData(referenceNumber)
|
|
20
30
|
console.log('Creating new transaction based on', referenceNumber)
|
|
21
31
|
try {
|
|
22
32
|
const response = await salesApi.createTransaction(transactionData)
|
|
23
33
|
console.log('New transaction created:', response)
|
|
34
|
+
return response
|
|
24
35
|
} catch (e) {
|
|
25
36
|
console.log('Error creating transaction', JSON.stringify(transactionData))
|
|
26
37
|
throw e
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
const takeRecurringPayment = async (agreementId, transaction) => {
|
|
42
|
+
const preparedPayment = preparePayment(agreementId, transaction)
|
|
43
|
+
console.log('Requesting payment:', preparedPayment)
|
|
44
|
+
const payment = await sendPayment(preparedPayment)
|
|
45
|
+
payments.push({
|
|
46
|
+
agreementId,
|
|
47
|
+
paymentId: payment.payment_id
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
const processPermissionData = async referenceNumber => {
|
|
31
52
|
console.log('Preparing data based on', referenceNumber)
|
|
32
53
|
const data = await salesApi.preparePermissionDataForRenewal(referenceNumber)
|
|
@@ -56,3 +77,15 @@ const prepareStartDate = permission => {
|
|
|
56
77
|
.utc()
|
|
57
78
|
.toISOString()
|
|
58
79
|
}
|
|
80
|
+
|
|
81
|
+
const preparePayment = (agreementId, transaction) => {
|
|
82
|
+
const result = {
|
|
83
|
+
amount: Math.round(transaction.cost * 100),
|
|
84
|
+
description: 'The recurring card payment for your rod fishing licence',
|
|
85
|
+
reference: transaction.id,
|
|
86
|
+
authorisation_mode: 'agreement',
|
|
87
|
+
agreement_id: agreementId
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { sendPayment } from '../govuk-pay-service.js'
|
|
2
|
+
import { govUkPayApi } from '@defra-fish/connectors-lib'
|
|
3
|
+
|
|
4
|
+
jest.mock('@defra-fish/connectors-lib')
|
|
5
|
+
|
|
6
|
+
describe('govuk-pay-service', () => {
|
|
7
|
+
describe('sendPayment', () => {
|
|
8
|
+
const preparedPayment = { id: '1234' }
|
|
9
|
+
|
|
10
|
+
it('sendPayment should return response from createPayment in json format', async () => {
|
|
11
|
+
const mockPreparedPayment = { id: 'test-payment-id' }
|
|
12
|
+
const mockResponse = { status: 'success', paymentId: 'abc123' }
|
|
13
|
+
|
|
14
|
+
const mockFetchResponse = {
|
|
15
|
+
json: jest.fn().mockResolvedValue(mockResponse)
|
|
16
|
+
}
|
|
17
|
+
govUkPayApi.createPayment.mockResolvedValue(mockFetchResponse)
|
|
18
|
+
|
|
19
|
+
const result = await sendPayment(mockPreparedPayment)
|
|
20
|
+
|
|
21
|
+
expect(result).toEqual(mockResponse)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should send provided payload data to Gov.UK Pay', async () => {
|
|
25
|
+
govUkPayApi.createPayment.mockResolvedValue({
|
|
26
|
+
ok: true,
|
|
27
|
+
json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' })
|
|
28
|
+
})
|
|
29
|
+
const unique = Symbol('payload')
|
|
30
|
+
const payload = {
|
|
31
|
+
amount: '100',
|
|
32
|
+
description: 'The recurring card payment for your rod fishing licence',
|
|
33
|
+
reference: unique
|
|
34
|
+
}
|
|
35
|
+
await sendPayment(payload)
|
|
36
|
+
expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should throw an error when the GOV.UK Pay connector raises an error', async () => {
|
|
40
|
+
govUkPayApi.createPayment.mockImplementationOnce(() => {
|
|
41
|
+
throw new Error('Oops!')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await sendPayment(preparedPayment)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
expect(e.message).toBe('Oops!')
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should log error message when the GOV.UK Pay API raises an error', async () => {
|
|
52
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
53
|
+
govUkPayApi.createPayment.mockImplementationOnce(() => {
|
|
54
|
+
throw new Error()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await sendPayment(preparedPayment)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
expect(consoleSpy).toHaveBeenCalledWith('Error creating payment', preparedPayment.id)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { govUkPayApi } from '@defra-fish/connectors-lib'
|
|
2
|
+
|
|
3
|
+
export const sendPayment = async preparedPayment => {
|
|
4
|
+
try {
|
|
5
|
+
const response = await govUkPayApi.createPayment(preparedPayment, true)
|
|
6
|
+
return await response.json()
|
|
7
|
+
} catch (e) {
|
|
8
|
+
console.error('Error creating payment', preparedPayment.id)
|
|
9
|
+
throw e
|
|
10
|
+
}
|
|
11
|
+
}
|