@defra-fish/recurring-payments-job 1.58.0-rc.9 → 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-rc.9",
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-rc.9",
40
- "@defra-fish/connectors-lib": "1.58.0-rc.9",
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": "ab360ee257b615b39da44fb8446c901f3a2ebb21"
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([{ expanded: { activePermission: { entity: { referenceNumber } } } }])
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([{ expanded: { activePermission: { entity: { referenceNumber: '1' } } } }])
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([{ expanded: { activePermission: { entity: { referenceNumber: '1' } } } }])
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([{ expanded: { activePermission: { entity: { referenceNumber: '1' } } } }])
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([{ expanded: { activePermission: { entity: { referenceNumber: '1' } } } }])
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([{ expanded: { activePermission: { entity: { referenceNumber: '1' } } } }])
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({ expanded: { activePermission: { entity: { referenceNumber: reference } } } })
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({ expanded: { activePermission: { entity: { referenceNumber: i } } } })
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
+ }