@defra-fish/gafl-webapp-service 1.55.0 → 1.56.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/gafl-webapp-service",
3
- "version": "1.55.0",
3
+ "version": "1.56.0",
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",
40
- "@defra-fish/connectors-lib": "1.55.0",
39
+ "@defra-fish/business-rules-lib": "1.56.0",
40
+ "@defra-fish/connectors-lib": "1.56.0",
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": "3405322b81a2016ba22e530de8d096a49be0bea4"
83
+ "gitHead": "4edcdd349b077ad0bdb3e7b4029df3aba7c04b49"
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 { prepareRecurringPayment } from '../../processors/payment.js'
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: async () => {}
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
- transaction: {
65
- get: async () => transaction,
66
- set: () => {}
71
+ overrides: {
72
+ transaction: {
73
+ get: async () => transaction,
74
+ set: () => {}
75
+ }
67
76
  }
68
77
  })
69
78
  await agreedHandler(mockRequest, getRequestToolkit())
70
- expect(prepareRecurringPayment).toHaveBeenCalledWith(mockRequest, transaction)
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
- prepareRecurringPayment.mockImplementationOnce((_p1, tp) => {
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("doesn't overwrite transaction id if one is already set", async () => {
94
- const setTransaction = jest.fn()
95
- const transactionId = 'abc-123-def-456'
96
- uuidv4.mockReturnValue('def-789-ghi-012')
97
- salesApi.finaliseTransaction.mockReturnValueOnce({
98
- permissions: []
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
- const mockRequest = {
101
- cache: () => ({
102
- helpers: {
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]: true,
107
- [COMPLETION_STATUS.finalised]: false,
108
- [RECURRING_PAYMENT]: false
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
- await agreedHandler(mockRequest, getRequestToolkit())
164
+ getPaymentStatus.mockReturnValueOnce({ state: { finished: true, status: 'success' } })
121
165
 
122
- expect(salesApi.finaliseTransaction).toHaveBeenCalledWith(
123
- transactionId,
124
- undefined // prepareApiFinalisationPayload has no mocked return value
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, prepareRecurringPayment } from '../processors/payment.js'
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 prepareRecurringPayment(request, transaction)
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')
@@ -466,11 +466,11 @@
466
466
  "licence_type_payment_edge_case": "Mae’n rhaid i chi gwblhau eich taliad cyn 11:30pm ar 31 Mawrth 2024 i dalu’r pris a ddangosir",
467
467
  "licence_type_radio_salmon_hint": "Mae'n cynnwys brithyllod a physgod bras (hyd at 3 gwialen)",
468
468
  "licence_type_radio_salmon": "Eogiaid a brithyllod y môr",
469
- "licence_type_radio_salmon_payment_summary": "eogiaid a brithyllod y môr",
469
+ "licence_type_radio_salmon_payment_summary": "Eogiaid a brithyllod y môr",
470
470
  "licence_type_radio_trout_three_rod": "Brithyllod a physgod bras (hyd at 3 gwialen)",
471
- "licence_type_radio_trout_three_rod_payment_summary": "brithyllod a physgod bras (hyd at 3 gwialen)",
471
+ "licence_type_radio_trout_three_rod_payment_summary": "Brithyllod a physgod bras (hyd at 3 gwialen)",
472
472
  "licence_type_radio_trout_two_rod": "Brithyllod a physgod bras (hyd at 2 wialen)",
473
- "licence_type_radio_trout_two_rod_payment_summary": "brithyllod a physgod bras (hyd at 2 wialen)",
473
+ "licence_type_radio_trout_two_rod_payment_summary": "Brithyllod a physgod bras (hyd at 2 wialen)",
474
474
  "licence_type_rules": "rheolau pysgota â gwialen (yn agor ar dudalen newydd)",
475
475
  "licence_type_salmon_acr_note_1": "Yn ôl y gyfraith, mae'n rhaid i chi roi gwybod am ",
476
476
  "licence_type_salmon_acr_note_2": " ffurflen daliadau (yn agor ar dudalen newydd)",
@@ -466,11 +466,11 @@
466
466
  "licence_type_payment_edge_case": "You must complete payment before 11:30pm on 31 March 2024 to get the price shown",
467
467
  "licence_type_radio_salmon_hint": "Includes trout and coarse (up to 3 rods)",
468
468
  "licence_type_radio_salmon": "Salmon and sea trout",
469
- "licence_type_radio_salmon_payment_summary": "salmon and sea trout",
469
+ "licence_type_radio_salmon_payment_summary": "Salmon and sea trout",
470
470
  "licence_type_radio_trout_three_rod": "Trout and coarse (up to 3 rods)",
471
- "licence_type_radio_trout_three_rod_payment_summary": "trout and coarse (up to 3 rods)",
471
+ "licence_type_radio_trout_three_rod_payment_summary": "Trout and coarse (up to 3 rods)",
472
472
  "licence_type_radio_trout_two_rod": "Trout and coarse (up to 2 rods)",
473
- "licence_type_radio_trout_two_rod_payment_summary": "trout and coarse (up to 2 rods)",
473
+ "licence_type_radio_trout_two_rod_payment_summary": "Trout and coarse (up to 2 rods)",
474
474
  "licence_type_rules": "rod fishing rules (opens in new tab)",
475
475
  "licence_type_salmon_acr_note_1": "Licence holders must by law ",
476
476
  "licence_type_salmon_acr_note_2": " report a catch return (opens in new tab)",
@@ -246,6 +246,127 @@ Array [
246
246
  ]
247
247
  `;
248
248
 
249
+ exports[`licence-summary > route licence summary rows creates licence summary name rows for 1 year new three rod licence 1`] = `
250
+ Array [
251
+ Object {
252
+ "actions": Object {
253
+ "items": Array [
254
+ Object {
255
+ "attributes": Object {
256
+ "id": "change-name",
257
+ },
258
+ "href": "/buy/name",
259
+ "text": "contact_summary_change",
260
+ "visuallyHiddenText": "licence_summary_name",
261
+ },
262
+ ],
263
+ },
264
+ "key": Object {
265
+ "text": "licence_summary_name",
266
+ },
267
+ "value": Object {
268
+ "html": "Brenin Pysgotwr",
269
+ },
270
+ },
271
+ Object {
272
+ "actions": Object {
273
+ "items": Array [
274
+ Object {
275
+ "attributes": Object {
276
+ "id": "change-birth-date",
277
+ },
278
+ "href": "/buy/date-of-birth",
279
+ "text": "contact_summary_change",
280
+ "visuallyHiddenText": "licence_summary_dob",
281
+ },
282
+ ],
283
+ },
284
+ "key": Object {
285
+ "text": "licence_summary_dob",
286
+ },
287
+ "value": Object {
288
+ "html": "1st January 1946",
289
+ },
290
+ },
291
+ Object {
292
+ "actions": Object {
293
+ "items": Array [
294
+ Object {
295
+ "attributes": Object {
296
+ "id": "change-licence-type",
297
+ },
298
+ "href": "/buy/licence-type",
299
+ "text": "contact_summary_change",
300
+ "visuallyHiddenText": "licence_summary_type",
301
+ },
302
+ ],
303
+ },
304
+ "key": Object {
305
+ "text": "licence_summary_type",
306
+ },
307
+ "value": Object {
308
+ "html": "Special Canal Licence, Shopping Trollies and Old Wellies",
309
+ },
310
+ },
311
+ Object {
312
+ "key": Object {
313
+ "text": "licence_summary_length",
314
+ },
315
+ "value": Object {
316
+ "html": "licence_type_12m",
317
+ },
318
+ },
319
+ Object {
320
+ "actions": Object {
321
+ "items": Array [
322
+ Object {
323
+ "attributes": Object {
324
+ "id": "change-licence-to-start",
325
+ },
326
+ "href": "/buy/start-kind",
327
+ "text": "contact_summary_change",
328
+ "visuallyHiddenText": "licence_summary_start_date",
329
+ },
330
+ ],
331
+ },
332
+ "key": Object {
333
+ "text": "licence_summary_start_date",
334
+ },
335
+ "value": Object {
336
+ "html": "30licence_summary_minutes_after_payment",
337
+ },
338
+ },
339
+ Object {
340
+ "actions": Object {
341
+ "items": Array [
342
+ Object {
343
+ "attributes": Object {
344
+ "id": "change-benefit-check",
345
+ },
346
+ "href": "/buy/disability-concession",
347
+ "text": "contact_summary_change",
348
+ "visuallyHiddenText": "licence_summary_ni_num",
349
+ },
350
+ ],
351
+ },
352
+ "key": Object {
353
+ "text": "licence_summary_ni_num",
354
+ },
355
+ "value": Object {
356
+ "html": "AB 12 34 56 A",
357
+ },
358
+ },
359
+ Object {
360
+ "key": Object {
361
+ "text": "damage",
362
+ },
363
+ "value": Object {
364
+ "html": "#6",
365
+ },
366
+ },
367
+ ]
368
+ `;
369
+
249
370
  exports[`licence-summary > route licence summary rows creates licence summary name rows for 1 year renewal 1`] = `
250
371
  Array [
251
372
  Object {
@@ -116,7 +116,7 @@ const getMockPermission = (licenseeOverrides = {}) => ({
116
116
  licenceToStart: 'after-payment',
117
117
  licenceStartDate: '2022-11-10',
118
118
  licenceType: 'Trout and coarse',
119
- numberOfRods: '3',
119
+ numberOfRods: '2',
120
120
  permit: { cost: 6 }
121
121
  })
122
122
 
@@ -372,16 +372,17 @@ describe('licence-summary > route', () => {
372
372
 
373
373
  describe('licence summary rows', () => {
374
374
  it.each`
375
- desc | currentPermission
376
- ${'1 year renewal'} | ${getMockPermission()}
377
- ${'1 year new licence'} | ${getMockNewPermission()}
378
- ${'1 year senior renewal'} | ${getMockSeniorPermission()}
379
- ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }}
380
- ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }}
381
- ${'Junior licence'} | ${getMockJuniorPermission()}
382
- ${'Blue badge concession'} | ${getMockBlueBadgePermission()}
383
- ${'Continuing permission'} | ${getMockContinuingPermission()}
384
- ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }}
375
+ desc | currentPermission
376
+ ${'1 year renewal'} | ${getMockPermission()}
377
+ ${'1 year new licence'} | ${getMockNewPermission()}
378
+ ${'1 year senior renewal'} | ${getMockSeniorPermission()}
379
+ ${'8 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '8D' }}
380
+ ${'1 day licence'} | ${{ ...getMockNewPermission(), licenceLength: '1D' }}
381
+ ${'Junior licence'} | ${getMockJuniorPermission()}
382
+ ${'Blue badge concession'} | ${getMockBlueBadgePermission()}
383
+ ${'Continuing permission'} | ${getMockContinuingPermission()}
384
+ ${'Another date permission'} | ${{ ...getMockPermission(), licenceToStart: 'another-date' }}
385
+ ${'1 year new three rod licence '} | ${{ ...getMockNewPermission(), numberOfRods: '3' }}
385
386
  `('creates licence summary name rows for $desc', async ({ currentPermission }) => {
386
387
  const mockRequest = getMockRequest({ currentPermission })
387
388
  const data = await getData(mockRequest)
@@ -113,12 +113,13 @@ class RowGenerator {
113
113
  }
114
114
 
115
115
  generateLicenceLengthRow () {
116
- return this.generateStandardRow(
117
- 'licence_summary_length',
118
- this.labels[`licence_type_${this.permission.licenceLength.toLowerCase()}`],
119
- LICENCE_LENGTH.uri,
120
- 'change-licence-length'
121
- )
116
+ const args = ['licence_summary_length', this.labels[`licence_type_${this.permission.licenceLength.toLowerCase()}`]]
117
+
118
+ if (this.permission.numberOfRods !== '3') {
119
+ args.push(LICENCE_LENGTH.uri, 'change-licence-length')
120
+ }
121
+
122
+ return this.generateStandardRow(...args)
122
123
  }
123
124
  }
124
125
 
@@ -63,7 +63,7 @@ describe('prepareApiTransactionPayload', () => {
63
63
  })
64
64
  })
65
65
 
66
- it('adds transaction id to payload', async () => {
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, prepareRecurringPayment } from '../payment.js'
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 = ({ isLicenceForYou = true, additionalPermissions = [], cost = 12, licenseeOverrides = {} } = {}) => ({
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('prepareRecurringPayment', () => {
241
+ describe('prepareRecurringPaymentAgreement', () => {
217
242
  it('reference equals transaction.id', async () => {
218
243
  const transaction = createTransaction()
219
- const result = await prepareRecurringPayment(createRequest(), transaction)
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 prepareRecurringPayment(request, transaction)
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 prepareRecurringPayment(request, transaction)
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 prepareRecurringPayment = async (request, transaction) => {
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