@defra-fish/gafl-webapp-service 1.55.0-rc.3 → 1.55.0-rc.4
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/handlers/__tests__/agreed-handler-recurring-payments.spec.js +94 -40
- package/src/handlers/agreed-handler.js +13 -5
- package/src/processors/__tests__/api-transaction.spec.js +9 -1
- package/src/processors/__tests__/payment.spec.js +33 -8
- package/src/processors/api-transaction.js +3 -2
- package/src/processors/payment.js +7 -3
- package/src/services/payment/__test__/govuk-pay-service.spec.js +253 -1
- package/src/services/payment/govuk-pay-service.js +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra-fish/gafl-webapp-service",
|
|
3
|
-
"version": "1.55.0-rc.
|
|
3
|
+
"version": "1.55.0-rc.4",
|
|
4
4
|
"description": "The websales frontend for the GAFL service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"prepare": "gulp --gulpfile build/gulpfile.cjs"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@defra-fish/business-rules-lib": "1.55.0-rc.
|
|
40
|
-
"@defra-fish/connectors-lib": "1.55.0-rc.
|
|
39
|
+
"@defra-fish/business-rules-lib": "1.55.0-rc.4",
|
|
40
|
+
"@defra-fish/connectors-lib": "1.55.0-rc.4",
|
|
41
41
|
"@defra/hapi-gapi": "^2.0.0",
|
|
42
42
|
"@hapi/boom": "^9.1.2",
|
|
43
43
|
"@hapi/catbox-redis": "^6.0.2",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"./gafl-jest-matchers.js"
|
|
81
81
|
]
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "621875e02b13ff01b77fbc16271c15d38f1900f5"
|
|
84
84
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { salesApi } from '@defra-fish/connectors-lib'
|
|
2
2
|
import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../../constants.js'
|
|
3
3
|
import agreedHandler from '../agreed-handler.js'
|
|
4
|
-
import {
|
|
5
|
-
import { sendRecurringPayment } from '../../services/payment/govuk-pay-service.js'
|
|
4
|
+
import { preparePayment, prepareRecurringPaymentAgreement } from '../../processors/payment.js'
|
|
5
|
+
import { sendPayment, sendRecurringPayment, getPaymentStatus } from '../../services/payment/govuk-pay-service.js'
|
|
6
6
|
import { prepareApiTransactionPayload } from '../../processors/api-transaction.js'
|
|
7
7
|
import { v4 as uuidv4 } from 'uuid'
|
|
8
8
|
import db from 'debug'
|
|
@@ -10,7 +10,13 @@ import db from 'debug'
|
|
|
10
10
|
jest.mock('@defra-fish/connectors-lib')
|
|
11
11
|
jest.mock('../../processors/payment.js')
|
|
12
12
|
jest.mock('../../services/payment/govuk-pay-service.js', () => ({
|
|
13
|
-
sendPayment: jest.fn()
|
|
13
|
+
sendPayment: jest.fn(() => ({
|
|
14
|
+
payment_id: 'payment-id-1',
|
|
15
|
+
_links: {
|
|
16
|
+
next_url: { href: 'next-url' },
|
|
17
|
+
self: { href: 'self-url' }
|
|
18
|
+
}
|
|
19
|
+
})),
|
|
14
20
|
getPaymentStatus: jest.fn(),
|
|
15
21
|
sendRecurringPayment: jest.fn(() => ({ agreementId: 'agr-eem-ent-id1' }))
|
|
16
22
|
}))
|
|
@@ -32,12 +38,12 @@ describe('The agreed handler', () => {
|
|
|
32
38
|
})
|
|
33
39
|
beforeEach(jest.clearAllMocks)
|
|
34
40
|
|
|
35
|
-
const getMockRequest = (overrides = {}) => ({
|
|
41
|
+
const getMockRequest = ({ overrides = {}, transactionSet = () => {} } = {}) => ({
|
|
36
42
|
cache: () => ({
|
|
37
43
|
helpers: {
|
|
38
44
|
transaction: {
|
|
39
45
|
get: async () => ({ cost: 0 }),
|
|
40
|
-
set:
|
|
46
|
+
set: transactionSet
|
|
41
47
|
},
|
|
42
48
|
status: {
|
|
43
49
|
get: async () => ({
|
|
@@ -54,6 +60,7 @@ describe('The agreed handler', () => {
|
|
|
54
60
|
})
|
|
55
61
|
|
|
56
62
|
const getRequestToolkit = () => ({
|
|
63
|
+
redirect: jest.fn(),
|
|
57
64
|
redirectWithLanguageCode: jest.fn()
|
|
58
65
|
})
|
|
59
66
|
|
|
@@ -61,18 +68,20 @@ describe('The agreed handler', () => {
|
|
|
61
68
|
it('sends the request and transaction to prepare the recurring payment', async () => {
|
|
62
69
|
const transaction = { cost: 0 }
|
|
63
70
|
const mockRequest = getMockRequest({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
overrides: {
|
|
72
|
+
transaction: {
|
|
73
|
+
get: async () => transaction,
|
|
74
|
+
set: () => {}
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
})
|
|
69
78
|
await agreedHandler(mockRequest, getRequestToolkit())
|
|
70
|
-
expect(
|
|
79
|
+
expect(prepareRecurringPaymentAgreement).toHaveBeenCalledWith(mockRequest, transaction)
|
|
71
80
|
})
|
|
72
81
|
|
|
73
82
|
it('adds a v4 guid to the transaction as an id', async () => {
|
|
74
83
|
let transactionPayload = null
|
|
75
|
-
|
|
84
|
+
prepareRecurringPaymentAgreement.mockImplementationOnce((_p1, tp) => {
|
|
76
85
|
transactionPayload = { ...tp }
|
|
77
86
|
})
|
|
78
87
|
const v4guid = Symbol('v4guid')
|
|
@@ -90,46 +99,73 @@ describe('The agreed handler', () => {
|
|
|
90
99
|
expect(transactionPayload.id).toBe(v4guid)
|
|
91
100
|
})
|
|
92
101
|
|
|
93
|
-
it(
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
it('sends a recurring payment agreement creation request to Gov.UK Pay', async () => {
|
|
103
|
+
const preparedPayment = Symbol('preparedPayment')
|
|
104
|
+
prepareRecurringPaymentAgreement.mockResolvedValueOnce(preparedPayment)
|
|
105
|
+
await agreedHandler(getMockRequest(), getRequestToolkit())
|
|
106
|
+
expect(sendRecurringPayment).toHaveBeenCalledWith(preparedPayment)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('when there is a cost and recurringAgreement status is set to true', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
salesApi.createTransaction.mockResolvedValueOnce({
|
|
112
|
+
id: 'transaction-id-1',
|
|
113
|
+
cost: 100
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('calls preparePayment', async () => {
|
|
118
|
+
const transaction = { id: Symbol('transaction') }
|
|
119
|
+
const request = getMockRequest({
|
|
120
|
+
overrides: {
|
|
121
|
+
transaction: {
|
|
122
|
+
get: async () => transaction,
|
|
123
|
+
set: () => {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
const toolkit = getRequestToolkit()
|
|
128
|
+
|
|
129
|
+
await agreedHandler(request, toolkit)
|
|
130
|
+
expect(preparePayment).toHaveBeenCalledWith(request, transaction)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('calls sendPayment with recurring as true', async () => {
|
|
134
|
+
const preparedPayment = Symbol('preparedPayment')
|
|
135
|
+
preparePayment.mockReturnValueOnce(preparedPayment)
|
|
136
|
+
|
|
137
|
+
await agreedHandler(getMockRequest(), getRequestToolkit())
|
|
138
|
+
expect(sendPayment).toHaveBeenCalledWith(preparedPayment, true)
|
|
99
139
|
})
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
140
|
+
|
|
141
|
+
it('calls getPaymentStatus with recurring as true', async () => {
|
|
142
|
+
const id = Symbol('paymentId')
|
|
143
|
+
const transaction = { id: '123', payment: { payment_id: id } }
|
|
144
|
+
const request = getMockRequest({
|
|
145
|
+
overrides: {
|
|
146
|
+
transaction: {
|
|
147
|
+
get: async () => transaction,
|
|
148
|
+
set: () => {}
|
|
149
|
+
},
|
|
103
150
|
status: {
|
|
104
151
|
get: async () => ({
|
|
105
152
|
[COMPLETION_STATUS.agreed]: true,
|
|
106
|
-
[COMPLETION_STATUS.posted]:
|
|
107
|
-
[COMPLETION_STATUS.finalised]:
|
|
108
|
-
[RECURRING_PAYMENT]:
|
|
153
|
+
[COMPLETION_STATUS.posted]: false,
|
|
154
|
+
[COMPLETION_STATUS.finalised]: true,
|
|
155
|
+
[RECURRING_PAYMENT]: true,
|
|
156
|
+
[COMPLETION_STATUS.paymentCreated]: true
|
|
109
157
|
}),
|
|
110
158
|
set: () => {}
|
|
111
|
-
},
|
|
112
|
-
transaction: {
|
|
113
|
-
get: async () => ({ cost: 0, id: transactionId }),
|
|
114
|
-
set: setTransaction
|
|
115
159
|
}
|
|
116
160
|
}
|
|
117
161
|
})
|
|
118
|
-
|
|
162
|
+
const toolkit = getRequestToolkit()
|
|
119
163
|
|
|
120
|
-
|
|
164
|
+
getPaymentStatus.mockReturnValueOnce({ state: { finished: true, status: 'success' } })
|
|
121
165
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('sends a recurring payment creation request to Gov.UK Pay', async () => {
|
|
129
|
-
const preparedPayment = Symbol('preparedPayment')
|
|
130
|
-
prepareRecurringPayment.mockResolvedValueOnce(preparedPayment)
|
|
131
|
-
await agreedHandler(getMockRequest(), getRequestToolkit())
|
|
132
|
-
expect(sendRecurringPayment).toHaveBeenCalledWith(preparedPayment)
|
|
166
|
+
await agreedHandler(request, toolkit)
|
|
167
|
+
expect(getPaymentStatus).toHaveBeenCalledWith(id, true)
|
|
168
|
+
})
|
|
133
169
|
})
|
|
134
170
|
|
|
135
171
|
// this doesn't really belong here, but until the other agreed handler tests are refactored to
|
|
@@ -140,7 +176,7 @@ describe('The agreed handler', () => {
|
|
|
140
176
|
|
|
141
177
|
await agreedHandler(getMockRequest(), getRequestToolkit())
|
|
142
178
|
|
|
143
|
-
expect(prepareApiTransactionPayload).toHaveBeenCalledWith(expect.any(Object), v4guid)
|
|
179
|
+
expect(prepareApiTransactionPayload).toHaveBeenCalledWith(expect.any(Object), v4guid, undefined)
|
|
144
180
|
})
|
|
145
181
|
|
|
146
182
|
it.each(['zxy-098-wvu-765', '467482f1-099d-403d-b6b3-8db7e70d19e3'])(
|
|
@@ -158,5 +194,23 @@ describe('The agreed handler', () => {
|
|
|
158
194
|
expect(debugMock).toHaveBeenCalledWith(`Created agreement with id ${agreement_id}`)
|
|
159
195
|
}
|
|
160
196
|
)
|
|
197
|
+
|
|
198
|
+
it.each(['zxy-098-wvu-765', '467482f1-099d-403d-b6b3-8db7e70d19e3'])(
|
|
199
|
+
"assigns agreement id '%s' to the transaction when recurring payment agreement created",
|
|
200
|
+
async agreementId => {
|
|
201
|
+
const mockTransactionCacheSet = jest.fn()
|
|
202
|
+
sendRecurringPayment.mockResolvedValueOnce({
|
|
203
|
+
agreement_id: agreementId
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
await agreedHandler(getMockRequest({ transactionSet: mockTransactionCacheSet }), getRequestToolkit())
|
|
207
|
+
|
|
208
|
+
expect(mockTransactionCacheSet).toHaveBeenCalledWith(
|
|
209
|
+
expect.objectContaining({
|
|
210
|
+
agreementId
|
|
211
|
+
})
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
)
|
|
161
215
|
})
|
|
162
216
|
})
|
|
@@ -14,7 +14,7 @@ import db from 'debug'
|
|
|
14
14
|
import { salesApi } from '@defra-fish/connectors-lib'
|
|
15
15
|
import { prepareApiTransactionPayload, prepareApiFinalisationPayload } from '../processors/api-transaction.js'
|
|
16
16
|
import { sendPayment, getPaymentStatus, sendRecurringPayment } from '../services/payment/govuk-pay-service.js'
|
|
17
|
-
import { preparePayment,
|
|
17
|
+
import { preparePayment, prepareRecurringPaymentAgreement } from '../processors/payment.js'
|
|
18
18
|
import { COMPLETION_STATUS, RECURRING_PAYMENT } from '../constants.js'
|
|
19
19
|
import { ORDER_COMPLETE, PAYMENT_CANCELLED, PAYMENT_FAILED } from '../uri.js'
|
|
20
20
|
import { PAYMENT_JOURNAL_STATUS_CODES, GOVUK_PAY_ERROR_STATUS_CODES } from '@defra-fish/business-rules-lib'
|
|
@@ -29,7 +29,7 @@ const debug = db('webapp:agreed-handler')
|
|
|
29
29
|
* @returns {Promise<*>}
|
|
30
30
|
*/
|
|
31
31
|
const sendToSalesApi = async (request, transaction, status) => {
|
|
32
|
-
const apiTransactionPayload = await prepareApiTransactionPayload(request, transaction.id)
|
|
32
|
+
const apiTransactionPayload = await prepareApiTransactionPayload(request, transaction.id, transaction.agreementId)
|
|
33
33
|
let response
|
|
34
34
|
try {
|
|
35
35
|
response = await salesApi.createTransaction(apiTransactionPayload)
|
|
@@ -63,7 +63,7 @@ const createRecurringPayment = async (request, transaction, status) => {
|
|
|
63
63
|
/*
|
|
64
64
|
* Prepare the payment payload
|
|
65
65
|
*/
|
|
66
|
-
const preparedPayment = await
|
|
66
|
+
const preparedPayment = await prepareRecurringPaymentAgreement(request, transaction)
|
|
67
67
|
|
|
68
68
|
/*
|
|
69
69
|
* Send the prepared payment to the GOV.UK pay API using the connector
|
|
@@ -72,7 +72,11 @@ const createRecurringPayment = async (request, transaction, status) => {
|
|
|
72
72
|
|
|
73
73
|
debug(`Created agreement with id ${paymentResponse.agreement_id}`)
|
|
74
74
|
status[COMPLETION_STATUS.recurringAgreement] = true
|
|
75
|
+
|
|
76
|
+
transaction.agreementId = paymentResponse.agreement_id
|
|
77
|
+
|
|
75
78
|
await request.cache().helpers.status.set(status)
|
|
79
|
+
await request.cache().helpers.transaction.set(transaction)
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
@@ -88,6 +92,8 @@ const createRecurringPayment = async (request, transaction, status) => {
|
|
|
88
92
|
* @returns {Promise<void>}
|
|
89
93
|
*/
|
|
90
94
|
const createPayment = async (request, transaction, status) => {
|
|
95
|
+
const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true
|
|
96
|
+
|
|
91
97
|
/*
|
|
92
98
|
* Prepare the payment payload
|
|
93
99
|
*/
|
|
@@ -96,7 +102,7 @@ const createPayment = async (request, transaction, status) => {
|
|
|
96
102
|
/*
|
|
97
103
|
* Send the prepared payment to the GOV.UK pay API using the connector
|
|
98
104
|
*/
|
|
99
|
-
const paymentResponse = await sendPayment(preparedPayment)
|
|
105
|
+
const paymentResponse = await sendPayment(preparedPayment, recurring)
|
|
100
106
|
|
|
101
107
|
/*
|
|
102
108
|
* Used by the payment mop up job, create the payment journal entry which is removed when the user completes the journey
|
|
@@ -143,10 +149,12 @@ const createPayment = async (request, transaction, status) => {
|
|
|
143
149
|
* @returns {Promise<void>}
|
|
144
150
|
*/
|
|
145
151
|
const processPayment = async (request, transaction, status) => {
|
|
152
|
+
const recurring = status && status[COMPLETION_STATUS.recurringAgreement] === true
|
|
153
|
+
|
|
146
154
|
/*
|
|
147
155
|
* Get the payment status
|
|
148
156
|
*/
|
|
149
|
-
const { state } = await getPaymentStatus(transaction.payment.payment_id)
|
|
157
|
+
const { state } = await getPaymentStatus(transaction.payment.payment_id, recurring)
|
|
150
158
|
|
|
151
159
|
if (!state.finished) {
|
|
152
160
|
throw Boom.forbidden('Attempt to access the agreed handler during payment journey')
|
|
@@ -63,7 +63,7 @@ describe('prepareApiTransactionPayload', () => {
|
|
|
63
63
|
})
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
it('adds
|
|
66
|
+
it('adds transactionId to payload', async () => {
|
|
67
67
|
const transactionId = Symbol('transactionId')
|
|
68
68
|
|
|
69
69
|
const payload = await prepareApiTransactionPayload(getMockRequest(), transactionId)
|
|
@@ -71,6 +71,14 @@ describe('prepareApiTransactionPayload', () => {
|
|
|
71
71
|
expect(payload.transactionId).toBe(transactionId)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
+
it('adds agreementId to payload', async () => {
|
|
75
|
+
const agreementId = Symbol('agreementId')
|
|
76
|
+
|
|
77
|
+
const payload = await prepareApiTransactionPayload(getMockRequest(), 'transaction_id', agreementId)
|
|
78
|
+
|
|
79
|
+
expect(payload.agreementId).toBe(agreementId)
|
|
80
|
+
})
|
|
81
|
+
|
|
74
82
|
const getMockRequest = (overrides = {}, state = {}) => ({
|
|
75
83
|
cache: () => ({
|
|
76
84
|
helpers: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { preparePayment,
|
|
1
|
+
import { preparePayment, prepareRecurringPaymentAgreement } from '../payment.js'
|
|
2
2
|
import { licenceTypeAndLengthDisplay } from '../licence-type-display.js'
|
|
3
3
|
import { addLanguageCodeToUri } from '../uri-helper.js'
|
|
4
4
|
import { AGREED } from '../../uri.js'
|
|
@@ -20,9 +20,16 @@ const createRequest = (opts = {}, catalog = {}) => ({
|
|
|
20
20
|
server: { info: { protocol: opts.protocol || '' } }
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
-
const createTransaction = ({
|
|
23
|
+
const createTransaction = ({
|
|
24
|
+
isLicenceForYou = true,
|
|
25
|
+
additionalPermissions = [],
|
|
26
|
+
cost = 12,
|
|
27
|
+
licenseeOverrides = {},
|
|
28
|
+
agreementId
|
|
29
|
+
} = {}) => ({
|
|
24
30
|
id: 'transaction-id',
|
|
25
31
|
cost,
|
|
32
|
+
agreementId,
|
|
26
33
|
permissions: [
|
|
27
34
|
{
|
|
28
35
|
id: 'permission-id',
|
|
@@ -49,7 +56,7 @@ describe('preparePayment', () => {
|
|
|
49
56
|
it.each(['http', 'https'])('uses SSL when "x-forwarded-proto" header is present, proto "%s"', proto => {
|
|
50
57
|
addLanguageCodeToUri.mockReturnValue(proto + '://localhost:1234/buy/agreed')
|
|
51
58
|
const request = createRequest({ headers: { 'x-forwarded-proto': proto } })
|
|
52
|
-
const result = preparePayment(request, createTransaction())
|
|
59
|
+
const result = preparePayment(request, createTransaction(), false)
|
|
53
60
|
|
|
54
61
|
expect(result.return_url).toBe(`${proto}://localhost:1234/buy/agreed`)
|
|
55
62
|
})
|
|
@@ -211,12 +218,30 @@ describe('preparePayment', () => {
|
|
|
211
218
|
expect(result.email).toBe(undefined)
|
|
212
219
|
})
|
|
213
220
|
})
|
|
221
|
+
|
|
222
|
+
describe('if agreementId is not present', () => {
|
|
223
|
+
it('does not include set_up_agreement', () => {
|
|
224
|
+
const result = preparePayment(createRequest(), createTransaction())
|
|
225
|
+
expect(result.set_up_agreement).toBe(undefined)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
describe('if agreementId is present', () => {
|
|
230
|
+
it('set_up_agreement is set to agreementId', () => {
|
|
231
|
+
const agreementId = 'foo'
|
|
232
|
+
const recurringPaymentTransaction = createTransaction({ agreementId })
|
|
233
|
+
|
|
234
|
+
const result = preparePayment(createRequest(), recurringPaymentTransaction)
|
|
235
|
+
|
|
236
|
+
expect(result.set_up_agreement).toBe(agreementId)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
214
239
|
})
|
|
215
240
|
|
|
216
|
-
describe('
|
|
241
|
+
describe('prepareRecurringPaymentAgreement', () => {
|
|
217
242
|
it('reference equals transaction.id', async () => {
|
|
218
243
|
const transaction = createTransaction()
|
|
219
|
-
const result = await
|
|
244
|
+
const result = await prepareRecurringPaymentAgreement(createRequest(), transaction)
|
|
220
245
|
expect(result.reference).toBe(transaction.id)
|
|
221
246
|
})
|
|
222
247
|
|
|
@@ -227,7 +252,7 @@ describe('prepareRecurringPayment', () => {
|
|
|
227
252
|
const request = createRequest({}, mockCatalog)
|
|
228
253
|
const transaction = createTransaction()
|
|
229
254
|
|
|
230
|
-
const result = await
|
|
255
|
+
const result = await prepareRecurringPaymentAgreement(request, transaction)
|
|
231
256
|
expect(result.description).toBe(mockCatalog.recurring_payment_description)
|
|
232
257
|
})
|
|
233
258
|
|
|
@@ -235,7 +260,7 @@ describe('prepareRecurringPayment', () => {
|
|
|
235
260
|
const transaction = createTransaction()
|
|
236
261
|
const request = createRequest()
|
|
237
262
|
|
|
238
|
-
const result = await
|
|
239
|
-
expect(debug).toHaveBeenCalledWith('Creating prepared recurring payment %o', result)
|
|
263
|
+
const result = await prepareRecurringPaymentAgreement(request, transaction)
|
|
264
|
+
expect(debug).toHaveBeenCalledWith('Creating prepared recurring payment agreement %o', result)
|
|
240
265
|
})
|
|
241
266
|
})
|
|
@@ -7,7 +7,7 @@ import { countries } from './refdata-helper.js'
|
|
|
7
7
|
import { salesApi } from '@defra-fish/connectors-lib'
|
|
8
8
|
import { licenceToStart } from '../pages/licence-details/licence-to-start/update-transaction.js'
|
|
9
9
|
|
|
10
|
-
export const prepareApiTransactionPayload = async (request, transactionId) => {
|
|
10
|
+
export const prepareApiTransactionPayload = async (request, transactionId, agreementId) => {
|
|
11
11
|
const transactionCache = await request.cache().helpers.transaction.get()
|
|
12
12
|
const concessions = await salesApi.concessions.getAll()
|
|
13
13
|
const countryList = await countries.getAll()
|
|
@@ -63,7 +63,8 @@ export const prepareApiTransactionPayload = async (request, transactionId) => {
|
|
|
63
63
|
request.state && request.state[process.env.OIDC_SESSION_COOKIE_NAME]
|
|
64
64
|
? request.state[process.env.OIDC_SESSION_COOKIE_NAME].oid
|
|
65
65
|
: undefined,
|
|
66
|
-
transactionId
|
|
66
|
+
transactionId,
|
|
67
|
+
agreementId
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -50,17 +50,21 @@ export const preparePayment = (request, transaction) => {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
if (transaction.agreementId) {
|
|
54
|
+
result.set_up_agreement = transaction.agreementId
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
debug('Creating prepared payment %o', result)
|
|
54
58
|
return result
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
export const
|
|
61
|
+
export const prepareRecurringPaymentAgreement = async (request, transaction) => {
|
|
58
62
|
debug('Preparing recurring payment %s', JSON.stringify(transaction, undefined, '\t'))
|
|
59
|
-
// The recurring card payment for your rod fishing licence
|
|
63
|
+
// The recurring card payment agreement for your rod fishing licence
|
|
60
64
|
const result = {
|
|
61
65
|
reference: transaction.id,
|
|
62
66
|
description: request.i18n.getCatalog().recurring_payment_description
|
|
63
67
|
}
|
|
64
|
-
debug('Creating prepared recurring payment %o', result)
|
|
68
|
+
debug('Creating prepared recurring payment agreement %o', result)
|
|
65
69
|
return result
|
|
66
70
|
}
|
|
@@ -2,7 +2,7 @@ import mockTransaction from './data/mock-transaction.js'
|
|
|
2
2
|
import { preparePayment } from '../../../processors/payment.js'
|
|
3
3
|
import { AGREED } from '../../../uri.js'
|
|
4
4
|
import { addLanguageCodeToUri } from '../../../processors/uri-helper.js'
|
|
5
|
-
import { sendRecurringPayment } from '../govuk-pay-service.js'
|
|
5
|
+
import { sendPayment, sendRecurringPayment, getPaymentStatus } from '../govuk-pay-service.js'
|
|
6
6
|
import { govUkPayApi } from '@defra-fish/connectors-lib'
|
|
7
7
|
import db from 'debug'
|
|
8
8
|
const { value: debug } = db.mock.results[db.mock.calls.findIndex(c => c[0] === 'webapp:govuk-pay-service')]
|
|
@@ -158,6 +158,142 @@ describe('The govuk-pay-service', () => {
|
|
|
158
158
|
console.log(preparedPayment)
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
+
describe('sendPayment', () => {
|
|
162
|
+
const preparedPayment = {
|
|
163
|
+
id: '1234',
|
|
164
|
+
user_identifier: 'test-user'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
jest.clearAllMocks()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it.each([
|
|
172
|
+
[true, true],
|
|
173
|
+
[false, false],
|
|
174
|
+
[false, undefined]
|
|
175
|
+
])('should call the govUkPayApi with recurring as %s if the argument is %s', async (expected, value) => {
|
|
176
|
+
const mockResponse = {
|
|
177
|
+
ok: true,
|
|
178
|
+
json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' })
|
|
179
|
+
}
|
|
180
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
181
|
+
const unique = Symbol('payload')
|
|
182
|
+
const payload = { unique }
|
|
183
|
+
await sendPayment(payload, value)
|
|
184
|
+
expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, expected)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should send provided payload data to Gov.UK Pay', async () => {
|
|
188
|
+
const mockResponse = {
|
|
189
|
+
ok: true,
|
|
190
|
+
json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' })
|
|
191
|
+
}
|
|
192
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
193
|
+
const unique = Symbol('payload')
|
|
194
|
+
const payload = {
|
|
195
|
+
reference: 'd81f1a2b-6508-468f-8342-b6770f60f7cd',
|
|
196
|
+
description: 'Fishing permission',
|
|
197
|
+
user_identifier: '1218c1c5-38e4-4bf3-81ea-9cbce3994d30',
|
|
198
|
+
unique
|
|
199
|
+
}
|
|
200
|
+
await sendPayment(payload)
|
|
201
|
+
expect(govUkPayApi.createPayment).toHaveBeenCalledWith(payload, false)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should return response body when payment creation is successful', async () => {
|
|
205
|
+
const mockResponse = {
|
|
206
|
+
ok: true,
|
|
207
|
+
json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' })
|
|
208
|
+
}
|
|
209
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
210
|
+
|
|
211
|
+
const result = await sendPayment(preparedPayment)
|
|
212
|
+
|
|
213
|
+
expect(result).toEqual({ success: true, paymentId: 'abc123' })
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should log debug message when response.ok is true', async () => {
|
|
217
|
+
const mockResponse = {
|
|
218
|
+
ok: true,
|
|
219
|
+
json: jest.fn().mockResolvedValue({ success: true, paymentId: 'abc123' })
|
|
220
|
+
}
|
|
221
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
222
|
+
|
|
223
|
+
await sendPayment(preparedPayment)
|
|
224
|
+
|
|
225
|
+
expect(debug).toHaveBeenCalledWith('Successful payment creation response: %o', { success: true, paymentId: 'abc123' })
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should log error message when response.ok is false', async () => {
|
|
229
|
+
const mockResponse = {
|
|
230
|
+
ok: false,
|
|
231
|
+
status: 500,
|
|
232
|
+
json: jest.fn().mockResolvedValue({ message: 'Server error' })
|
|
233
|
+
}
|
|
234
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
235
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await sendPayment(preparedPayment)
|
|
239
|
+
} catch (error) {
|
|
240
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Failure creating payment in the GOV.UK API service', {
|
|
241
|
+
transactionId: preparedPayment.id,
|
|
242
|
+
method: 'POST',
|
|
243
|
+
payload: preparedPayment,
|
|
244
|
+
status: mockResponse.status,
|
|
245
|
+
response: { message: 'Server error' }
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should throw error when API call fails with network issue', async () => {
|
|
251
|
+
const mockError = new Error('Network error')
|
|
252
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
253
|
+
govUkPayApi.createPayment.mockRejectedValue(mockError)
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
await sendPayment(preparedPayment)
|
|
257
|
+
} catch (error) {
|
|
258
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
259
|
+
`Error creating payment in the GOV.UK API service - tid: ${preparedPayment.id}`,
|
|
260
|
+
mockError
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should throw error for when rate limit is breached', async () => {
|
|
266
|
+
const mockResponse = {
|
|
267
|
+
ok: false,
|
|
268
|
+
status: 429,
|
|
269
|
+
json: jest.fn().mockResolvedValue({ message: 'Rate limit exceeded' })
|
|
270
|
+
}
|
|
271
|
+
const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(jest.fn())
|
|
272
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await sendPayment(preparedPayment)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(`GOV.UK Pay API rate limit breach - tid: ${preparedPayment.id}`)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should throw error for unexpected response status', async () => {
|
|
282
|
+
const mockResponse = {
|
|
283
|
+
ok: false,
|
|
284
|
+
status: 500,
|
|
285
|
+
json: jest.fn().mockResolvedValue({ message: 'Server error' })
|
|
286
|
+
}
|
|
287
|
+
govUkPayApi.createPayment.mockResolvedValue(mockResponse)
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await sendPayment(preparedPayment)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
expect(error.message).toBe('Unexpected response from GOV.UK pay API')
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
161
297
|
describe('sendRecurringPayment', () => {
|
|
162
298
|
const preparedPayment = {
|
|
163
299
|
id: '1234',
|
|
@@ -277,4 +413,120 @@ describe('The govuk-pay-service', () => {
|
|
|
277
413
|
}
|
|
278
414
|
})
|
|
279
415
|
})
|
|
416
|
+
|
|
417
|
+
describe('getPaymentStatus', () => {
|
|
418
|
+
const paymentId = '1234'
|
|
419
|
+
|
|
420
|
+
beforeEach(() => {
|
|
421
|
+
jest.clearAllMocks()
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it.each([
|
|
425
|
+
[true, true],
|
|
426
|
+
[false, false],
|
|
427
|
+
[false, undefined]
|
|
428
|
+
])('should call the govUkPayApi with recurring as %s if the argument is %s', async (expected, value) => {
|
|
429
|
+
const mockResponse = { ok: true, status: 200, json: () => {} }
|
|
430
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
431
|
+
await getPaymentStatus(paymentId, value)
|
|
432
|
+
expect(govUkPayApi.fetchPaymentStatus).toHaveBeenCalledWith(paymentId, expected)
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('should send provided paymentId to Gov.UK Pay', async () => {
|
|
436
|
+
const mockResponse = { ok: true, status: 200, json: () => {} }
|
|
437
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
438
|
+
await getPaymentStatus(paymentId)
|
|
439
|
+
expect(govUkPayApi.fetchPaymentStatus).toHaveBeenCalledWith(paymentId, false)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should return response body when payment status check is successful', async () => {
|
|
443
|
+
const resBody = Symbol('body')
|
|
444
|
+
const mockResponse = { ok: true, status: 200, json: jest.fn().mockResolvedValue(resBody) }
|
|
445
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
446
|
+
|
|
447
|
+
const result = await getPaymentStatus(paymentId)
|
|
448
|
+
|
|
449
|
+
expect(result).toEqual(resBody)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should log debug message when response.ok is true', async () => {
|
|
453
|
+
const resBody = Symbol('body')
|
|
454
|
+
const mockResponse = { ok: true, status: 200, json: jest.fn().mockResolvedValue(resBody) }
|
|
455
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
456
|
+
|
|
457
|
+
await getPaymentStatus(paymentId)
|
|
458
|
+
|
|
459
|
+
expect(debug).toHaveBeenCalledWith('Payment status response: %o', resBody)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('should log error message when response.ok is false', async () => {
|
|
463
|
+
const mockResponse = {
|
|
464
|
+
ok: false,
|
|
465
|
+
status: 500,
|
|
466
|
+
json: jest.fn().mockResolvedValue({ message: 'Server error' })
|
|
467
|
+
}
|
|
468
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
469
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
await getPaymentStatus(paymentId)
|
|
473
|
+
} catch (error) {
|
|
474
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
475
|
+
`Error retrieving the payment status from the GOV.UK API service - tid: ${paymentId}`,
|
|
476
|
+
{
|
|
477
|
+
method: 'GET',
|
|
478
|
+
paymentId: paymentId,
|
|
479
|
+
status: mockResponse.status,
|
|
480
|
+
response: { message: 'Server error' }
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
it('should throw error when API call fails with network issue', async () => {
|
|
487
|
+
const mockError = new Error('Network error')
|
|
488
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(jest.fn())
|
|
489
|
+
govUkPayApi.fetchPaymentStatus.mockRejectedValue(mockError)
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
await getPaymentStatus(paymentId)
|
|
493
|
+
} catch (error) {
|
|
494
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
495
|
+
`Error retrieving the payment status from the GOV.UK API service - paymentId: ${paymentId}`,
|
|
496
|
+
mockError
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
it('should throw error for when rate limit is breached', async () => {
|
|
502
|
+
const mockResponse = {
|
|
503
|
+
ok: false,
|
|
504
|
+
status: 429,
|
|
505
|
+
json: jest.fn().mockResolvedValue({ message: 'Rate limit exceeded' })
|
|
506
|
+
}
|
|
507
|
+
const consoleErrorSpy = jest.spyOn(console, 'info').mockImplementation(jest.fn())
|
|
508
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
await getPaymentStatus(paymentId)
|
|
512
|
+
} catch (error) {
|
|
513
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(`GOV.UK Pay API rate limit breach - paymentId: ${paymentId}`)
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should throw error for unexpected response status', async () => {
|
|
518
|
+
const mockResponse = {
|
|
519
|
+
ok: false,
|
|
520
|
+
status: 500,
|
|
521
|
+
json: jest.fn().mockResolvedValue({ message: 'Server error' })
|
|
522
|
+
}
|
|
523
|
+
govUkPayApi.fetchPaymentStatus.mockResolvedValue(mockResponse)
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
await getPaymentStatus(paymentId)
|
|
527
|
+
} catch (error) {
|
|
528
|
+
expect(error.message).toBe('Unexpected response from GOV.UK pay API')
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
})
|
|
280
532
|
})
|
|
@@ -46,10 +46,10 @@ const getTransactionErrorMessage = async (transactionId, payload, response) => (
|
|
|
46
46
|
* @param preparedPayment - the prepared payload for the payment. See in processors/payment.js
|
|
47
47
|
* @returns {Promise<*>}
|
|
48
48
|
*/
|
|
49
|
-
export const sendPayment = async preparedPayment => {
|
|
49
|
+
export const sendPayment = async (preparedPayment, recurring = false) => {
|
|
50
50
|
let response
|
|
51
51
|
try {
|
|
52
|
-
response = await govUkPayApi.createPayment(preparedPayment)
|
|
52
|
+
response = await govUkPayApi.createPayment(preparedPayment, recurring)
|
|
53
53
|
} catch (err) {
|
|
54
54
|
/*
|
|
55
55
|
* Potentially errors caught here (unreachable, timeouts) may be retried - set origin on the error to indicate
|
|
@@ -78,11 +78,11 @@ export const sendPayment = async preparedPayment => {
|
|
|
78
78
|
* @param paymentId - the paymentId
|
|
79
79
|
* @returns {Promise<any>}
|
|
80
80
|
*/
|
|
81
|
-
export const getPaymentStatus = async paymentId => {
|
|
81
|
+
export const getPaymentStatus = async (paymentId, recurring = false) => {
|
|
82
82
|
debug(`Get payment status for paymentId: ${paymentId}`)
|
|
83
83
|
let response
|
|
84
84
|
try {
|
|
85
|
-
response = await govUkPayApi.fetchPaymentStatus(paymentId)
|
|
85
|
+
response = await govUkPayApi.fetchPaymentStatus(paymentId, recurring)
|
|
86
86
|
} catch (err) {
|
|
87
87
|
/*
|
|
88
88
|
* Errors caught here (unreachable, timeouts) may be retried - set origin on the error to indicate
|