@defra/forms-engine-plugin 4.0.61 → 4.0.62

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.
Files changed (56) hide show
  1. package/.server/server/plugins/engine/components/PaymentField.d.ts +3 -17
  2. package/.server/server/plugins/engine/components/PaymentField.js +12 -3
  3. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
  4. package/.server/server/plugins/engine/helpers.d.ts +1 -0
  5. package/.server/server/plugins/engine/plugin.js +3 -1
  6. package/.server/server/plugins/engine/plugin.js.map +1 -1
  7. package/.server/server/plugins/engine/routes/payment-helper.d.ts +3 -1
  8. package/.server/server/plugins/engine/routes/payment-helper.js +5 -5
  9. package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -1
  10. package/.server/server/plugins/engine/routes/payment-helper.test.js +4 -2
  11. package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -1
  12. package/.server/server/plugins/engine/routes/payment.js +7 -1
  13. package/.server/server/plugins/engine/routes/payment.js.map +1 -1
  14. package/.server/server/plugins/engine/services/formsService.d.ts +7 -0
  15. package/.server/server/plugins/engine/services/formsService.js +11 -0
  16. package/.server/server/plugins/engine/services/formsService.js.map +1 -1
  17. package/.server/server/plugins/engine/services/formsService.test.js +4 -1
  18. package/.server/server/plugins/engine/services/formsService.test.js.map +1 -1
  19. package/.server/server/plugins/engine/types.d.ts +5 -2
  20. package/.server/server/plugins/engine/types.js.map +1 -1
  21. package/.server/server/plugins/payment/helper.d.ts +4 -11
  22. package/.server/server/plugins/payment/helper.js +11 -19
  23. package/.server/server/plugins/payment/helper.js.map +1 -1
  24. package/.server/server/plugins/payment/helper.test.js +1 -22
  25. package/.server/server/plugins/payment/helper.test.js.map +1 -1
  26. package/.server/server/plugins/payment/service.d.ts +3 -3
  27. package/.server/server/plugins/payment/service.js +25 -15
  28. package/.server/server/plugins/payment/service.js.map +1 -1
  29. package/.server/server/plugins/payment/service.test.js +8 -6
  30. package/.server/server/plugins/payment/service.test.js.map +1 -1
  31. package/.server/server/types.d.ts +1 -0
  32. package/.server/server/types.js.map +1 -1
  33. package/.server/server/utils/file-form-service.js +10 -0
  34. package/.server/server/utils/file-form-service.js.map +1 -1
  35. package/.server/server/utils/file-form-service.test.js +5 -0
  36. package/.server/server/utils/file-form-service.test.js.map +1 -1
  37. package/.server/typings/hapi/index.d.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/server/plugins/engine/beta/form-context.test.ts +2 -1
  40. package/src/server/plugins/engine/components/PaymentField.test.ts +139 -5
  41. package/src/server/plugins/engine/components/PaymentField.ts +29 -21
  42. package/src/server/plugins/engine/plugin.ts +3 -1
  43. package/src/server/plugins/engine/routes/payment-helper.js +9 -5
  44. package/src/server/plugins/engine/routes/payment-helper.test.js +4 -1
  45. package/src/server/plugins/engine/routes/payment.js +8 -1
  46. package/src/server/plugins/engine/services/formsService.js +11 -0
  47. package/src/server/plugins/engine/services/formsService.test.js +6 -1
  48. package/src/server/plugins/engine/types.ts +6 -1
  49. package/src/server/plugins/payment/helper.js +15 -23
  50. package/src/server/plugins/payment/helper.test.js +1 -32
  51. package/src/server/plugins/payment/service.js +42 -28
  52. package/src/server/plugins/payment/service.test.js +22 -24
  53. package/src/server/types.ts +1 -0
  54. package/src/server/utils/file-form-service.js +11 -0
  55. package/src/server/utils/file-form-service.test.js +13 -0
  56. package/src/typings/hapi/index.d.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"file-form-service.test.js","names":["join","FormStatus","FileFormService","describe","service","beforeEach","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","live","addForm","import","meta","dirname","title","slug","it","getFormMetadata","expect","toBe","toThrow","getFormMetadataById","form","getFormDefinition","name","startPage","interfaceImpl","toFormsService","res1","res2","res3","Draft","readForm","rejects"],"sources":["../../../src/server/utils/file-form-service.test.js"],"sourcesContent":["import { join } from 'node:path'\n\nimport { FormStatus } from '~/src/server/routes/types.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\ndescribe('File-form-service', () => {\n /** @type {FileFormService} */\n let service\n beforeEach(async () => {\n const now = new Date()\n const user = { id: 'user', displayName: 'Username' }\n const author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n }\n service = new FileFormService()\n const metadata = {\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: 'email@domain.com',\n ...author,\n live: author\n }\n await service.addForm(\n `${join(import.meta.dirname, '../../../test/form/definitions')}/components.json`,\n {\n ...metadata,\n id: '95e92559-968d-44ae-8666-2b1ad3dffd31',\n title: 'Form test',\n slug: 'form-test'\n }\n )\n })\n\n describe('metadata by slug', () => {\n it('should get form metadata by slug', () => {\n const meta = service.getFormMetadata('form-test')\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadata('form-test-missing')).toThrow(\n \"Form metadata 'form-test-missing' not found\"\n )\n })\n })\n\n describe('metadata by id', () => {\n it('should get form metadata by id', () => {\n const meta = service.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadataById('id-missing')).toThrow(\n \"Form metadata id 'id-missing' not found\"\n )\n })\n })\n\n describe('definition by id', () => {\n it('should get form definition by id', () => {\n const form = service.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(form.name).toBe('All components')\n expect(form.startPage).toBe('/all-components')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormDefinition('id-missing')).toThrow(\n \"Form definition 'id-missing' not found\"\n )\n })\n })\n\n describe('toFormsService', () => {\n it('should create interface', async () => {\n const interfaceImpl = service.toFormsService()\n const res1 = await interfaceImpl.getFormMetadata('form-test')\n expect(res1.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res1.title).toBe('Form test')\n\n const res2 = await interfaceImpl.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(res2.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res2.title).toBe('Form test')\n\n const res3 = await interfaceImpl.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31',\n FormStatus.Draft\n )\n expect(res3?.name).toBe('All components')\n expect(res3?.startPage).toBe('/all-components')\n })\n })\n\n describe('readForm', () => {\n it('should throw if invalid extension', async () => {\n await expect(\n service.readForm('/some-folder/some-file.bad')\n ).rejects.toThrow(\"Invalid file extension '.bad'\")\n })\n })\n})\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"file-form-service.test.js","names":["join","FormStatus","FileFormService","describe","service","beforeEach","now","Date","user","id","displayName","author","createdAt","createdBy","updatedAt","updatedBy","metadata","organisation","teamName","teamEmail","submissionGuidance","notificationEmail","live","addForm","import","meta","dirname","title","slug","it","getFormMetadata","expect","toBe","toThrow","getFormMetadataById","form","getFormDefinition","name","startPage","interfaceImpl","toFormsService","res1","res2","res3","Draft","res4","getFormSecret","process","env","PAYMENT_PROVIDER_API_KEY_TEST","res5","readForm","rejects"],"sources":["../../../src/server/utils/file-form-service.test.js"],"sourcesContent":["import { join } from 'node:path'\n\nimport { FormStatus } from '~/src/server/routes/types.js'\nimport { FileFormService } from '~/src/server/utils/file-form-service.js'\n\ndescribe('File-form-service', () => {\n /** @type {FileFormService} */\n let service\n beforeEach(async () => {\n const now = new Date()\n const user = { id: 'user', displayName: 'Username' }\n const author = {\n createdAt: now,\n createdBy: user,\n updatedAt: now,\n updatedBy: user\n }\n service = new FileFormService()\n const metadata = {\n organisation: 'Defra',\n teamName: 'Team name',\n teamEmail: 'team@defra.gov.uk',\n submissionGuidance: \"Thanks for your submission, we'll be in touch\",\n notificationEmail: 'email@domain.com',\n ...author,\n live: author\n }\n await service.addForm(\n `${join(import.meta.dirname, '../../../test/form/definitions')}/components.json`,\n {\n ...metadata,\n id: '95e92559-968d-44ae-8666-2b1ad3dffd31',\n title: 'Form test',\n slug: 'form-test'\n }\n )\n })\n\n describe('metadata by slug', () => {\n it('should get form metadata by slug', () => {\n const meta = service.getFormMetadata('form-test')\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadata('form-test-missing')).toThrow(\n \"Form metadata 'form-test-missing' not found\"\n )\n })\n })\n\n describe('metadata by id', () => {\n it('should get form metadata by id', () => {\n const meta = service.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(meta.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(meta.title).toBe('Form test')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormMetadataById('id-missing')).toThrow(\n \"Form metadata id 'id-missing' not found\"\n )\n })\n })\n\n describe('definition by id', () => {\n it('should get form definition by id', () => {\n const form = service.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(form.name).toBe('All components')\n expect(form.startPage).toBe('/all-components')\n })\n\n it('should throw if not found', () => {\n expect(() => service.getFormDefinition('id-missing')).toThrow(\n \"Form definition 'id-missing' not found\"\n )\n })\n })\n\n describe('toFormsService', () => {\n it('should create interface', async () => {\n const interfaceImpl = service.toFormsService()\n const res1 = await interfaceImpl.getFormMetadata('form-test')\n expect(res1.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res1.title).toBe('Form test')\n\n const res2 = await interfaceImpl.getFormMetadataById(\n '95e92559-968d-44ae-8666-2b1ad3dffd31'\n )\n expect(res2.id).toBe('95e92559-968d-44ae-8666-2b1ad3dffd31')\n expect(res2.title).toBe('Form test')\n\n const res3 = await interfaceImpl.getFormDefinition(\n '95e92559-968d-44ae-8666-2b1ad3dffd31',\n FormStatus.Draft\n )\n expect(res3?.name).toBe('All components')\n expect(res3?.startPage).toBe('/all-components')\n\n const res4 = await interfaceImpl.getFormSecret(\n '95e92559-968d-44ae-8666-2b1ad3dffd31',\n 'my-secret-name'\n )\n expect(res4).toBe('test-api-key')\n\n delete process.env.PAYMENT_PROVIDER_API_KEY_TEST\n const res5 = await interfaceImpl.getFormSecret(\n '95e92559-968d-44ae-8666-2b1ad3dffd31',\n 'my-secret-name'\n )\n expect(res5).toBe('')\n })\n })\n\n describe('readForm', () => {\n it('should throw if invalid extension', async () => {\n await expect(\n service.readForm('/some-folder/some-file.bad')\n ).rejects.toThrow(\"Invalid file extension '.bad'\")\n })\n })\n})\n"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["import { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport {\n type COMPONENT_STATE_ERROR,\n type EXTERNAL_STATE_APPENDAGE,\n type EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type AnyFormRequest,\n type FormSubmissionError,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/yar' {\n interface YarFlashes {\n [EXTERNAL_STATE_APPENDAGE]: object\n [EXTERNAL_STATE_PAYLOAD]: object\n [COMPONENT_STATE_ERROR]: string\n [key: string]: { errors: FormSubmissionError[] }\n }\n}\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: AnyRequest) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: AnyFormRequest | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndExit?: PluginOptions['saveAndExit']\n baseUrl: string\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"index.d.js","names":[],"sources":["../../../src/typings/hapi/index.d.ts"],"sourcesContent":["import { type Plugin } from '@hapi/hapi'\nimport { type ServerYar, type Yar } from '@hapi/yar'\nimport { type Logger } from 'pino'\n\nimport {\n type COMPONENT_STATE_ERROR,\n type EXTERNAL_STATE_APPENDAGE,\n type EXTERNAL_STATE_PAYLOAD\n} from '~/src/server/constants.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type AnyFormRequest,\n type FormSubmissionError,\n type PluginOptions\n} from '~/src/server/plugins/engine/types.ts'\nimport { type CacheService } from '~/src/server/services/index.js'\n\ndeclare module '@hapi/yar' {\n interface YarFlashes {\n [EXTERNAL_STATE_APPENDAGE]: object\n [EXTERNAL_STATE_PAYLOAD]: object\n [COMPONENT_STATE_ERROR]: string\n [key: string]: { errors: FormSubmissionError[] }\n }\n}\n\ndeclare module '@hapi/hapi' {\n // Here we are decorating Hapi interface types with\n // props from plugins which doesn't export @types\n interface PluginProperties {\n crumb: {\n generate?: (request: AnyRequest) => string\n }\n 'forms-engine-plugin': {\n baseLayoutPath: string\n cacheService: CacheService\n viewContext?: (\n request: AnyFormRequest | null\n ) => Record<string, unknown> | Promise<Record<string, unknown>>\n saveAndExit?: PluginOptions['saveAndExit']\n baseUrl: string\n services: PluginOptions['services']\n }\n }\n\n interface Request {\n logger: Logger\n yar: Yar\n }\n\n interface RequestApplicationState {\n model?: FormModel\n }\n\n interface Server {\n logger: Logger\n yar: ServerYar\n }\n\n interface ServerApplicationState {\n model?: FormModel\n models: Map<string, { model: FormModel; updatedAt: Date }>\n }\n}\n\ndeclare module 'blankie' {\n declare const blankie: {\n plugin: Plugin<Record<string, boolean | string | string[]>>\n }\n\n export = blankie\n}\n\ndeclare module 'blipp' {\n declare const blipp: {\n plugin: Plugin\n }\n\n export = blipp\n}\n\ndeclare module 'hapi-pulse' {\n declare const hapiPulse: {\n plugin: Plugin<{\n timeout: number\n }>\n }\n\n export = hapiPulse\n}\n"],"mappings":"","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.0.61",
3
+ "version": "4.0.62",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -150,7 +150,8 @@ describe('getFormModel helper', () => {
150
150
  formsService: {
151
151
  getFormMetadata: jest.fn().mockResolvedValue(metadata),
152
152
  getFormMetadataById: jest.fn(),
153
- getFormDefinition: jest.fn().mockResolvedValue(definition)
153
+ getFormDefinition: jest.fn().mockResolvedValue(definition),
154
+ getFormSecret: jest.fn()
154
155
  },
155
156
  formSubmissionService: {
156
157
  persistFiles: jest.fn(),
@@ -3,6 +3,7 @@ import {
3
3
  type FormMetadata,
4
4
  type PaymentFieldComponent
5
5
  } from '@defra/forms-model'
6
+ import { StatusCodes } from 'http-status-codes'
6
7
 
7
8
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
8
9
  import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
@@ -14,18 +15,26 @@ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
14
15
  import { PaymentPreAuthError } from '~/src/server/plugins/engine/pageControllers/errors.js'
15
16
  import {
16
17
  type FormContext,
17
- type FormValue
18
+ type FormValue,
19
+ type PaymentExternalArgs
18
20
  } from '~/src/server/plugins/engine/types.js'
19
21
  import {
20
22
  type FormRequestPayload,
21
23
  type FormResponseToolkit
22
24
  } from '~/src/server/routes/types.js'
23
25
  import { get, post, postJson } from '~/src/server/services/httpService.js'
26
+ import { type Services } from '~/src/server/types.js'
24
27
  import definition from '~/test/form/definitions/blank.js'
25
28
  import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
26
29
 
27
30
  jest.mock('~/src/server/services/httpService.ts')
28
31
 
32
+ const mockServices = {
33
+ formsService: {
34
+ getFormSecret: () => 'secret-value'
35
+ }
36
+ } as unknown as Services
37
+
29
38
  describe('PaymentField', () => {
30
39
  let model: FormModel
31
40
 
@@ -250,6 +259,7 @@ describe('PaymentField', () => {
250
259
 
251
260
  const collection = new ComponentCollection([def], { model })
252
261
  const paymentField = collection.fields[0] as PaymentField
262
+ paymentField.model = { services: mockServices } as unknown as FormModel
253
263
 
254
264
  describe('dispatcher', () => {
255
265
  it('should create payment and redirect to gov pay', async () => {
@@ -277,7 +287,8 @@ describe('PaymentField', () => {
277
287
  model: {
278
288
  formId: 'formid',
279
289
  basePath: 'base-path',
280
- name: 'PaymentModel'
290
+ name: 'PaymentModel',
291
+ services: mockServices
281
292
  },
282
293
  getState: jest
283
294
  .fn()
@@ -287,7 +298,7 @@ describe('PaymentField', () => {
287
298
  sourceUrl: 'http://localhost:3009/test-payment',
288
299
  isLive: false,
289
300
  isPreview: true
290
- }
301
+ } as unknown as PaymentExternalArgs
291
302
  // @ts-expect-error - partial mock
292
303
  jest.mocked(postJson).mockResolvedValueOnce({
293
304
  payload: {
@@ -342,7 +353,8 @@ describe('PaymentField', () => {
342
353
  model: {
343
354
  formId: 'formid',
344
355
  basePath: 'base-path',
345
- name: 'PaymentModel'
356
+ name: 'PaymentModel',
357
+ services: mockServices
346
358
  },
347
359
  getState: jest.fn().mockResolvedValueOnce({
348
360
  $$__referenceNumber: 'pay-ref-123',
@@ -361,7 +373,7 @@ describe('PaymentField', () => {
361
373
  sourceUrl: 'http://localhost:3009/test-payment',
362
374
  isLive: false,
363
375
  isPreview: true
364
- }
376
+ } as unknown as PaymentExternalArgs
365
377
 
366
378
  const res = await PaymentField.dispatcher(mockRequest, mockH, args)
367
379
 
@@ -372,6 +384,128 @@ describe('PaymentField', () => {
372
384
  expect(mockRedirectCode).toHaveBeenCalledWith(303)
373
385
  expect(postJson).not.toHaveBeenCalled()
374
386
  })
387
+
388
+ it('should display error if create payment fails (e.g. network or bad api key) - test payment', async () => {
389
+ const mockYarSet = jest.fn()
390
+ const mockYarFlash = jest.fn()
391
+ const mockRequest = {
392
+ server: {
393
+ plugins: {
394
+ // eslint-disable-next-line no-useless-computed-key
395
+ ['forms-engine-plugin']: {
396
+ baseUrl: 'base-url'
397
+ }
398
+ }
399
+ },
400
+ yar: {
401
+ set: mockYarSet,
402
+ flash: mockYarFlash
403
+ },
404
+ url: {
405
+ href: '/here'
406
+ }
407
+ } as unknown as FormRequestPayload
408
+ const mockH = {
409
+ redirect: jest
410
+ .fn()
411
+ .mockReturnValueOnce({ code: jest.fn().mockReturnValueOnce('ok') })
412
+ } as unknown as FormResponseToolkit
413
+ const args = {
414
+ controller: {
415
+ model: {
416
+ formId: 'formid',
417
+ basePath: 'base-path',
418
+ name: 'PaymentModel',
419
+ services: mockServices
420
+ },
421
+ getState: jest
422
+ .fn()
423
+ .mockResolvedValueOnce({ $$__referenceNumber: 'pay-ref-123' })
424
+ },
425
+ component: paymentField,
426
+ sourceUrl: 'http://localhost:3009/test-payment',
427
+ isLive: false,
428
+ isPreview: true
429
+ } as unknown as PaymentExternalArgs
430
+ jest.mocked(postJson).mockImplementationOnce(() => {
431
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
432
+ throw { output: { statusCode: StatusCodes.UNAUTHORIZED } }
433
+ })
434
+
435
+ const res = await PaymentField.dispatcher(mockRequest, mockH, args)
436
+ expect(res).toBe('ok')
437
+ expect(mockYarSet).not.toHaveBeenCalled()
438
+ expect(mockYarFlash).toHaveBeenCalledWith(
439
+ 'COMPONENT_STATE_ERROR',
440
+ {
441
+ href: '#myComponent',
442
+ name: 'myComponent',
443
+ text: 'Add a valid test API key before you can preview the payment journey.'
444
+ },
445
+ true
446
+ )
447
+ })
448
+
449
+ it('should display error if create payment fails (e.g. network or bad api key) - live payment', async () => {
450
+ const mockYarSet = jest.fn()
451
+ const mockYarFlash = jest.fn()
452
+ const mockRequest = {
453
+ server: {
454
+ plugins: {
455
+ // eslint-disable-next-line no-useless-computed-key
456
+ ['forms-engine-plugin']: {
457
+ baseUrl: 'base-url'
458
+ }
459
+ }
460
+ },
461
+ yar: {
462
+ set: mockYarSet,
463
+ flash: mockYarFlash
464
+ },
465
+ url: {
466
+ href: '/here'
467
+ }
468
+ } as unknown as FormRequestPayload
469
+ const mockH = {
470
+ redirect: jest
471
+ .fn()
472
+ .mockReturnValueOnce({ code: jest.fn().mockReturnValueOnce('ok') })
473
+ } as unknown as FormResponseToolkit
474
+ const args = {
475
+ controller: {
476
+ model: {
477
+ formId: 'formid',
478
+ basePath: 'base-path',
479
+ name: 'PaymentModel',
480
+ services: mockServices
481
+ },
482
+ getState: jest
483
+ .fn()
484
+ .mockResolvedValueOnce({ $$__referenceNumber: 'pay-ref-123' })
485
+ },
486
+ component: paymentField,
487
+ sourceUrl: 'http://localhost:3009/test-payment',
488
+ isLive: true,
489
+ isPreview: false
490
+ } as unknown as PaymentExternalArgs
491
+ jest.mocked(postJson).mockImplementationOnce(() => {
492
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
493
+ throw { output: { statusCode: StatusCodes.UNAUTHORIZED } }
494
+ })
495
+
496
+ const res = await PaymentField.dispatcher(mockRequest, mockH, args)
497
+ expect(res).toBe('ok')
498
+ expect(mockYarSet).not.toHaveBeenCalled()
499
+ expect(mockYarFlash).toHaveBeenCalledWith(
500
+ 'COMPONENT_STATE_ERROR',
501
+ {
502
+ href: '#myComponent',
503
+ name: 'myComponent',
504
+ text: 'There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.'
505
+ },
506
+ true
507
+ )
508
+ })
375
509
  })
376
510
 
377
511
  describe('onSubmit', () => {
@@ -7,16 +7,19 @@ import {
7
7
  import { StatusCodes } from 'http-status-codes'
8
8
  import joi, { type ObjectSchema } from 'joi'
9
9
 
10
+ import { COMPONENT_STATE_ERROR } from '~/src/server/constants.js'
10
11
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
11
12
  import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
12
- import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
13
+ import {
14
+ createError,
15
+ getPluginOptions
16
+ } from '~/src/server/plugins/engine/helpers.js'
13
17
  import {
14
18
  PaymentErrorTypes,
15
19
  PaymentPreAuthError,
16
20
  PaymentSubmissionError
17
21
  } from '~/src/server/plugins/engine/pageControllers/errors.js'
18
22
  import {
19
- type AnyFormRequest,
20
23
  type FormContext,
21
24
  type FormRequestPayload,
22
25
  type FormResponseToolkit
@@ -27,7 +30,8 @@ import {
27
30
  type FormState,
28
31
  type FormStateValue,
29
32
  type FormSubmissionError,
30
- type FormSubmissionState
33
+ type FormSubmissionState,
34
+ type PaymentExternalArgs
31
35
  } from '~/src/server/plugins/engine/types.js'
32
36
  import {
33
37
  createPaymentService,
@@ -186,7 +190,7 @@ export class PaymentField extends FormComponent {
186
190
  static async dispatcher(
187
191
  request: FormRequestPayload,
188
192
  h: FormResponseToolkit,
189
- args: PaymentDispatcherArgs
193
+ args: PaymentExternalArgs
190
194
  ): Promise<unknown> {
191
195
  const { options, name: componentName } = args.component
192
196
  const { model } = args.controller
@@ -205,7 +209,12 @@ export class PaymentField extends FormComponent {
205
209
 
206
210
  const isLivePayment = args.isLive && !args.isPreview
207
211
  const formId = args.controller.model.formId
208
- const paymentService = createPaymentService(isLivePayment, formId)
212
+ const formsService = model.services.formsService
213
+ const paymentService = await createPaymentService(
214
+ isLivePayment,
215
+ formId,
216
+ formsService
217
+ )
209
218
 
210
219
  const uuid = randomUUID()
211
220
 
@@ -229,6 +238,15 @@ export class PaymentField extends FormComponent {
229
238
  { formId, slug }
230
239
  )
231
240
 
241
+ if (!payment) {
242
+ const message = isLivePayment
243
+ ? 'There is a problem and we cannot take a payment. Contact us (details in the footer of this form) or save your progress and return to the form later.'
244
+ : 'Add a valid test API key before you can preview the payment journey.'
245
+ const govukError = createError(componentName, message)
246
+ request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
247
+ return h.redirect(request.url.href).code(StatusCodes.SEE_OTHER)
248
+ }
249
+
232
250
  const sessionData: PaymentSessionData = {
233
251
  uuid,
234
252
  formId,
@@ -272,7 +290,12 @@ export class PaymentField extends FormComponent {
272
290
  }
273
291
 
274
292
  const { paymentId, isLivePayment, formId } = paymentState
275
- const paymentService = createPaymentService(isLivePayment, formId)
293
+ const formsService = this.model.services.formsService
294
+ const paymentService = await createPaymentService(
295
+ isLivePayment,
296
+ formId,
297
+ formsService
298
+ )
276
299
 
277
300
  /**
278
301
  * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
@@ -343,21 +366,6 @@ export class PaymentField extends FormComponent {
343
366
  }
344
367
  }
345
368
 
346
- export interface PaymentDispatcherArgs {
347
- controller: {
348
- model: {
349
- formId: string
350
- basePath: string
351
- name: string
352
- }
353
- getState: (request: AnyFormRequest) => Promise<FormSubmissionState>
354
- }
355
- component: PaymentField
356
- sourceUrl: string
357
- isLive: boolean
358
- isPreview: boolean
359
- }
360
-
361
369
  /**
362
370
  * Session data stored when dispatching to GOV.UK Pay
363
371
  */
@@ -41,7 +41,8 @@ export const plugin = {
41
41
  onRequest,
42
42
  ordnanceSurveyApiKey,
43
43
  baseUrl,
44
- ordnanceSurveyApiSecret
44
+ ordnanceSurveyApiSecret,
45
+ services
45
46
  } = options
46
47
 
47
48
  const cacheService =
@@ -77,6 +78,7 @@ export const plugin = {
77
78
  server.expose('cacheService', cacheService)
78
79
  server.expose('saveAndExit', saveAndExit)
79
80
  server.expose('baseUrl', baseUrl)
81
+ server.expose('services', services)
80
82
 
81
83
  server.app.model = model
82
84
 
@@ -1,16 +1,16 @@
1
1
  import Boom from '@hapi/boom'
2
2
 
3
3
  import { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'
4
- import { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'
5
- import { PaymentService } from '~/src/server/plugins/payment/service.js'
4
+ import { createPaymentService } from '~/src/server/plugins/payment/helper.js'
6
5
 
7
6
  /**
8
7
  * Validates session data and retrieves payment status
9
8
  * @param {Request} request - the request
10
9
  * @param {string} uuid - the payment UUID
10
+ * @param {FormsService} formsService - the forms service
11
11
  * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
12
12
  */
13
- export async function getPaymentContext(request, uuid) {
13
+ export async function getPaymentContext(request, uuid, formsService) {
14
14
  const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`
15
15
  const session = /** @type {PaymentSessionData | null} */ (
16
16
  request.yar.get(sessionKey)
@@ -26,8 +26,11 @@ export async function getPaymentContext(request, uuid) {
26
26
  throw Boom.badRequest('No paymentId in session')
27
27
  }
28
28
 
29
- const apiKey = getPaymentApiKey(isLivePayment, formId)
30
- const paymentService = new PaymentService(apiKey)
29
+ const paymentService = await createPaymentService(
30
+ isLivePayment,
31
+ formId,
32
+ formsService
33
+ )
31
34
  const paymentStatus = await paymentService.getPaymentStatus(
32
35
  paymentId,
33
36
  isLivePayment
@@ -73,4 +76,5 @@ export function convertPenceToPounds(amount) {
73
76
  /**
74
77
  * @import { Request } from '@hapi/hapi'
75
78
  * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
79
+ * @import { FormsService } from '~/src/server/types.js'
76
80
  */
@@ -64,8 +64,11 @@ describe('payment helper', () => {
64
64
  error: undefined
65
65
  })
66
66
 
67
+ const mockFormsService = {
68
+ getFormSecret: () => 'secret-value'
69
+ }
67
70
  // @ts-expect-error - partial request mock
68
- const res = await getPaymentContext(mockRequest, uuid)
71
+ const res = await getPaymentContext(mockRequest, uuid, mockFormsService)
69
72
  expect(res).toEqual({
70
73
  paymentStatus: {
71
74
  paymentId: 'payment-id-12345',
@@ -4,6 +4,7 @@ import Joi from 'joi'
4
4
 
5
5
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
6
6
  import { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'
7
+ import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
7
8
  import {
8
9
  buildPaymentInfo,
9
10
  convertPenceToPounds,
@@ -128,9 +129,13 @@ function getReturnRoute() {
128
129
  path: PAYMENT_RETURN_PATH,
129
130
  async handler(request, h) {
130
131
  const { uuid } = /** @type {{ uuid: string }} */ (request.query)
132
+
133
+ const { services } = getPluginOptions(request.server)
134
+
131
135
  const { session, sessionKey, paymentStatus } = await getPaymentContext(
132
136
  request,
133
- uuid
137
+ uuid,
138
+ /** @type {FormsService} */ (services?.formsService)
134
139
  )
135
140
 
136
141
  /**
@@ -193,4 +198,6 @@ function getReturnRoute() {
193
198
  * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
194
199
  * @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
195
200
  * @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'
201
+ * @import { PluginOptions } from '~/src/server/plugins/engine/types.js'
202
+ * @import { FormsService } from '~/src/server/types.js'
196
203
  */
@@ -33,6 +33,17 @@ export function getFormDefinition(_id, _state) {
33
33
  throw error
34
34
  }
35
35
 
36
+ // eslint-disable-next-line jsdoc/require-returns-check
37
+ /**
38
+ * Dummy function to get a form secret.
39
+ * @param {string} _id - the id of the form
40
+ * @param {string} _secretName - the name of the secret
41
+ * @returns {Promise<string>}
42
+ */
43
+ export function getFormSecret(_id, _secretName) {
44
+ throw error
45
+ }
46
+
36
47
  /**
37
48
  * @import { FormStatus, FormDefinition, FormMetadata } from '@defra/forms-model'
38
49
  */
@@ -3,7 +3,8 @@ import { FormStatus } from '@defra/forms-model'
3
3
  import {
4
4
  getFormDefinition,
5
5
  getFormMetadata,
6
- getFormMetadataById
6
+ getFormMetadataById,
7
+ getFormSecret
7
8
  } from '~/src/server/plugins/engine/services/formsService.js'
8
9
 
9
10
  describe('formsService', () => {
@@ -18,4 +19,8 @@ describe('formsService', () => {
18
19
  it('getFormDefinition should throw error', () => {
19
20
  expect(() => getFormDefinition('id', FormStatus.Draft)).toThrow()
20
21
  })
22
+
23
+ it('getFormSecret should throw error', () => {
24
+ expect(() => getFormSecret('id', 'my-secret-name')).toThrow()
25
+ })
21
26
  })
@@ -5,6 +5,7 @@ import {
5
5
  type Item,
6
6
  type List,
7
7
  type Page,
8
+ type PaymentFieldComponent,
8
9
  type UkAddressFieldComponent
9
10
  } from '@defra/forms-model'
10
11
  import {
@@ -397,7 +398,7 @@ export interface ExternalArgs {
397
398
  component: ComponentDef
398
399
  controller: QuestionPageController
399
400
  sourceUrl: string
400
- actionArgs: Record<string, string>
401
+ actionArgs?: Record<string, string>
401
402
  isLive: boolean
402
403
  isPreview: boolean
403
404
  }
@@ -407,6 +408,10 @@ export interface PostcodeLookupExternalArgs extends ExternalArgs {
407
408
  actionArgs: { step: string }
408
409
  }
409
410
 
411
+ export interface PaymentExternalArgs extends ExternalArgs {
412
+ component: PaymentFieldComponent
413
+ }
414
+
410
415
  export interface ExternalStateAppendage {
411
416
  component: string
412
417
  data: FormStateValue | FormState
@@ -5,35 +5,23 @@ import { PaymentService } from '~/src/server/plugins/payment/service.js'
5
5
  export const DEFAULT_PAYMENT_HELP_URL =
6
6
  'https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs'
7
7
 
8
- /**
9
- * Determine which payment API key value to use.
10
- * If a draft preview form or a live preview form, read the TEST API key value specific to that form.
11
- * If a live (non-preview) form, read the LIVE API key value specific to that form.
12
- * @param {boolean} isLivePayment - true if this is a live payment (as opposed to a test one)
13
- * @param {string} formId - id of the form
14
- * @returns {string}
15
- */
16
- export function getPaymentApiKey(isLivePayment, formId) {
17
- const apiKeyValue = isLivePayment
18
- ? process.env[`PAYMENT_PROVIDER_API_KEY_LIVE_${formId}`]
19
- : process.env[`PAYMENT_PROVIDER_API_KEY_TEST_${formId}`]
20
-
21
- if (!apiKeyValue) {
22
- throw new Error(
23
- `[payment] Missing payment api key for ${isLivePayment ? 'live' : 'test'} form id ${formId}`
24
- )
25
- }
26
- return apiKeyValue
27
- }
8
+ const PAYMENT_TEST_API_KEY = 'payment-test-api-key'
9
+ const PAYMENT_LIVE_API_KEY = 'payment-live-api-key'
28
10
 
29
11
  /**
30
12
  * Creates a PaymentService instance with the appropriate API key
31
13
  * @param {boolean} isLivePayment - true if this is a live payment
32
14
  * @param {string} formId - id of the form
33
- * @returns {PaymentService}
15
+ * @param {FormsService} formsService - service to handle form data operations
16
+ * @returns {Promise<PaymentService>}
34
17
  */
35
- export function createPaymentService(isLivePayment, formId) {
36
- const apiKey = getPaymentApiKey(isLivePayment, formId)
18
+ export async function createPaymentService(
19
+ isLivePayment,
20
+ formId,
21
+ formsService
22
+ ) {
23
+ const secretName = isLivePayment ? PAYMENT_LIVE_API_KEY : PAYMENT_TEST_API_KEY
24
+ const apiKey = await formsService.getFormSecret(formId, secretName)
37
25
  return new PaymentService(apiKey)
38
26
  }
39
27
 
@@ -61,3 +49,7 @@ export function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') {
61
49
 
62
50
  return formatter.format(amount)
63
51
  }
52
+
53
+ /**
54
+ * @import { FormsService } from '~/src/server/types.js'
55
+ */
@@ -1,39 +1,8 @@
1
- import { config } from '~/src/config/index.js'
2
1
  import {
3
2
  formatCurrency,
4
- formatPaymentDate,
5
- getPaymentApiKey
3
+ formatPaymentDate
6
4
  } from '~/src/server/plugins/payment/helper.js'
7
5
 
8
- describe('getPaymentApiKey', () => {
9
- config.set('paymentProviderApiKeyTest', 'TEST-API-KEY')
10
- const formId = 'form-id'
11
- process.env['PAYMENT_PROVIDER_API_KEY_LIVE_form-id'] = 'LIVE-API-KEY'
12
- process.env['PAYMENT_PROVIDER_API_KEY_TEST_form-id'] = 'TEST-API-KEY'
13
-
14
- it('should read test key when non-live form', () => {
15
- const apiKey = getPaymentApiKey(false, formId)
16
- expect(apiKey).toBe('TEST-API-KEY')
17
- })
18
-
19
- it('should read live key when live form', () => {
20
- const apiKey = getPaymentApiKey(true, formId)
21
- expect(apiKey).toBe('LIVE-API-KEY')
22
- })
23
-
24
- it('should throw if TEST key is missing', () => {
25
- expect(() => getPaymentApiKey(false, 'form-id-missing')).toThrow(
26
- 'Missing payment api key for test form id form-id-missing'
27
- )
28
- })
29
-
30
- it('should throw if LIVE key is missing', () => {
31
- expect(() => getPaymentApiKey(true, 'form-id-missing')).toThrow(
32
- 'Missing payment api key for live form id form-id-missing'
33
- )
34
- })
35
- })
36
-
37
6
  describe('formatPaymentDate', () => {
38
7
  it('should format ISO date string to en-GB format', () => {
39
8
  const result = formatPaymentDate('2025-11-10T17:01:29.000Z')