@defra/forms-engine-plugin 2.1.4 → 2.1.6

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 (135) hide show
  1. package/.server/server/plugins/engine/components/ComponentBase.d.ts +2 -2
  2. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  3. package/.server/server/plugins/engine/components/ComponentCollection.d.ts +2 -2
  4. package/.server/server/plugins/engine/components/ComponentCollection.js +1 -1
  5. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  6. package/.server/server/plugins/engine/components/TelephoneNumberField.js +1 -1
  7. package/.server/server/plugins/engine/components/TelephoneNumberField.js.map +1 -1
  8. package/.server/server/plugins/engine/components/YesNoField.js +1 -1
  9. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  10. package/.server/server/plugins/engine/components/{helpers.d.ts → helpers/components.d.ts} +3 -10
  11. package/.server/server/plugins/engine/components/{helpers.js → helpers/components.js} +16 -29
  12. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -0
  13. package/.server/server/plugins/engine/components/helpers/index.d.ts +11 -0
  14. package/.server/server/plugins/engine/components/helpers/index.js +15 -0
  15. package/.server/server/plugins/engine/components/helpers/index.js.map +1 -0
  16. package/.server/server/plugins/engine/helpers.d.ts +1 -1
  17. package/.server/server/plugins/engine/helpers.js +1 -1
  18. package/.server/server/plugins/engine/helpers.js.map +1 -1
  19. package/.server/server/plugins/engine/models/FormModel.d.ts +2 -2
  20. package/.server/server/plugins/engine/models/FormModel.js +2 -2
  21. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  22. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +1 -1
  23. package/.server/server/plugins/engine/models/SummaryViewModel.js +1 -1
  24. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  25. package/.server/server/plugins/engine/models/types.d.ts +2 -2
  26. package/.server/server/plugins/engine/models/types.js.map +1 -1
  27. package/.server/server/plugins/engine/outputFormatters/adapter/v1.d.ts +6 -0
  28. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +23 -0
  29. package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -0
  30. package/.server/server/plugins/engine/outputFormatters/human/v1.d.ts +2 -2
  31. package/.server/server/plugins/engine/outputFormatters/human/v1.js +3 -2
  32. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  33. package/.server/server/plugins/engine/outputFormatters/index.d.ts +4 -3
  34. package/.server/server/plugins/engine/outputFormatters/index.js +4 -0
  35. package/.server/server/plugins/engine/outputFormatters/index.js.map +1 -1
  36. package/.server/server/plugins/engine/outputFormatters/machine/v1.d.ts +2 -2
  37. package/.server/server/plugins/engine/outputFormatters/machine/v1.js +2 -2
  38. package/.server/server/plugins/engine/outputFormatters/machine/v1.js.map +1 -1
  39. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +4 -1
  40. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  41. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +1 -1
  42. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  43. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +6 -5
  44. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  45. package/.server/server/plugins/engine/pageControllers/helpers/index.d.ts +9 -0
  46. package/.server/server/plugins/engine/pageControllers/helpers/index.js +15 -0
  47. package/.server/server/plugins/engine/pageControllers/helpers/index.js.map +1 -0
  48. package/.server/server/plugins/engine/pageControllers/{helpers.d.ts → helpers/pages.d.ts} +1 -10
  49. package/.server/server/plugins/engine/pageControllers/{helpers.js → helpers/pages.js} +2 -17
  50. package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -0
  51. package/.server/server/plugins/engine/routes/index.d.ts +1 -1
  52. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  53. package/.server/server/plugins/engine/routes/questions.js.map +1 -1
  54. package/.server/server/plugins/engine/services/localFormsService.d.ts +1 -1
  55. package/.server/server/plugins/engine/services/notifyService.d.ts +7 -2
  56. package/.server/server/plugins/engine/services/notifyService.js +12 -3
  57. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  58. package/.server/server/plugins/engine/types/index.d.ts +10 -0
  59. package/.server/server/plugins/engine/types/index.js +4 -0
  60. package/.server/server/plugins/engine/types/index.js.map +1 -0
  61. package/.server/server/plugins/engine/types/schema.d.ts +5 -0
  62. package/.server/server/plugins/engine/types/schema.js +24 -0
  63. package/.server/server/plugins/engine/types/schema.js.map +1 -0
  64. package/.server/server/plugins/engine/types.d.ts +39 -3
  65. package/.server/server/plugins/engine/types.js +4 -0
  66. package/.server/server/plugins/engine/types.js.map +1 -1
  67. package/.server/server/plugins/nunjucks/filters/answer.js +2 -2
  68. package/.server/server/plugins/nunjucks/filters/answer.js.map +1 -1
  69. package/.server/server/plugins/nunjucks/filters/answer.test.js +2 -2
  70. package/.server/server/plugins/nunjucks/filters/answer.test.js.map +1 -1
  71. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  72. package/.server/server/plugins/nunjucks/filters/page.d.ts +1 -1
  73. package/.server/server/types.d.ts +1 -1
  74. package/.server/server/types.js.map +1 -1
  75. package/package.json +6 -1
  76. package/src/server/plugins/engine/components/AutocompleteField.test.ts +1 -1
  77. package/src/server/plugins/engine/components/CheckboxesField.test.ts +1 -1
  78. package/src/server/plugins/engine/components/ComponentBase.ts +2 -2
  79. package/src/server/plugins/engine/components/ComponentCollection.ts +2 -2
  80. package/src/server/plugins/engine/components/DatePartsField.test.ts +1 -1
  81. package/src/server/plugins/engine/components/Details.test.ts +1 -1
  82. package/src/server/plugins/engine/components/EmailAddressField.test.ts +1 -1
  83. package/src/server/plugins/engine/components/FileUploadField.test.ts +2 -2
  84. package/src/server/plugins/engine/components/Html.test.ts +1 -1
  85. package/src/server/plugins/engine/components/InsetText.test.ts +1 -1
  86. package/src/server/plugins/engine/components/List.test.ts +1 -1
  87. package/src/server/plugins/engine/components/Markdown.test.ts +1 -1
  88. package/src/server/plugins/engine/components/MonthYearField.test.ts +1 -1
  89. package/src/server/plugins/engine/components/MultilineTextField.test.ts +1 -1
  90. package/src/server/plugins/engine/components/NumberField.test.ts +1 -1
  91. package/src/server/plugins/engine/components/RadiosField.test.ts +1 -1
  92. package/src/server/plugins/engine/components/SelectField.test.ts +1 -1
  93. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +1 -1
  94. package/src/server/plugins/engine/components/TelephoneNumberField.ts +1 -1
  95. package/src/server/plugins/engine/components/TextField.test.ts +1 -1
  96. package/src/server/plugins/engine/components/UkAddressField.test.ts +1 -1
  97. package/src/server/plugins/engine/components/YesNoField.test.ts +1 -1
  98. package/src/server/plugins/engine/components/YesNoField.ts +1 -1
  99. package/src/server/plugins/engine/components/{helpers.ts → helpers/components.ts} +41 -77
  100. package/src/server/plugins/engine/components/{helpers.test.ts → helpers/helpers.test.ts} +1 -1
  101. package/src/server/plugins/engine/components/helpers/index.ts +38 -0
  102. package/src/server/plugins/engine/helpers.test.ts +1 -1
  103. package/src/server/plugins/engine/helpers.ts +2 -2
  104. package/src/server/plugins/engine/models/FormModel.ts +2 -2
  105. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +1 -1
  106. package/src/server/plugins/engine/models/SummaryViewModel.ts +2 -2
  107. package/src/server/plugins/engine/models/types.ts +4 -4
  108. package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +506 -0
  109. package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +53 -0
  110. package/src/server/plugins/engine/outputFormatters/human/v1.ts +8 -6
  111. package/src/server/plugins/engine/outputFormatters/index.ts +11 -3
  112. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +1 -1
  113. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +7 -3
  114. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +1 -1
  115. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +1 -1
  116. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1 -1
  117. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +1 -1
  118. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +16 -5
  119. package/src/server/plugins/engine/pageControllers/{helpers.test.ts → helpers/helpers.test.ts} +2 -2
  120. package/src/server/plugins/engine/pageControllers/helpers/index.ts +20 -0
  121. package/src/server/plugins/engine/pageControllers/{helpers.ts → helpers/pages.ts} +1 -22
  122. package/src/server/plugins/engine/routes/index.ts +1 -1
  123. package/src/server/plugins/engine/routes/questions.test.ts +1 -1
  124. package/src/server/plugins/engine/routes/questions.ts +1 -1
  125. package/src/server/plugins/engine/services/notifyService.test.ts +156 -1
  126. package/src/server/plugins/engine/services/notifyService.ts +25 -4
  127. package/src/server/plugins/engine/types/index.ts +96 -0
  128. package/src/server/plugins/engine/types/schema.test.ts +152 -0
  129. package/src/server/plugins/engine/types/schema.ts +45 -0
  130. package/src/server/plugins/engine/types.ts +53 -3
  131. package/src/server/plugins/nunjucks/filters/answer.js +2 -2
  132. package/src/server/plugins/nunjucks/filters/answer.test.js +7 -4
  133. package/src/server/types.ts +2 -1
  134. package/.server/server/plugins/engine/components/helpers.js.map +0 -1
  135. package/.server/server/plugins/engine/pageControllers/helpers.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  hasComponentsEvenIfNoNext,
3
+ type FormMetadata,
3
4
  type Page,
4
5
  type SubmitPayload
5
6
  } from '@defra/forms-model'
@@ -8,7 +9,7 @@ import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
8
9
 
9
10
  import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
10
11
  import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
11
- import { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'
12
+ import { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'
12
13
  import {
13
14
  checkEmailAddressForLiveFormSubmission,
14
15
  checkFormStatus,
@@ -111,7 +112,8 @@ export class SummaryPageController extends QuestionPageController {
111
112
  const { getFormMetadata } = formsService
112
113
 
113
114
  // Get the form metadata using the `slug` param
114
- const { notificationEmail } = await getFormMetadata(params.slug)
115
+ const formMetadata = await getFormMetadata(params.slug)
116
+ const { notificationEmail } = formMetadata
115
117
  const { isPreview } = checkFormStatus(request.params)
116
118
  const emailAddress = notificationEmail ?? this.model.def.outputEmail
117
119
 
@@ -120,7 +122,14 @@ export class SummaryPageController extends QuestionPageController {
120
122
  // Send submission email
121
123
  if (emailAddress) {
122
124
  const viewModel = this.getSummaryViewModel(request, context)
123
- await submitForm(context, request, viewModel, model, emailAddress)
125
+ await submitForm(
126
+ context,
127
+ request,
128
+ viewModel,
129
+ model,
130
+ emailAddress,
131
+ formMetadata
132
+ )
124
133
  }
125
134
 
126
135
  await cacheService.setConfirmationState(request, { confirmed: true })
@@ -150,7 +159,8 @@ async function submitForm(
150
159
  request: FormRequestPayload,
151
160
  summaryViewModel: SummaryViewModel,
152
161
  model: FormModel,
153
- emailAddress: string
162
+ emailAddress: string,
163
+ formMetadata: FormMetadata
154
164
  ) {
155
165
  await extendFileRetention(model, context.state, emailAddress)
156
166
 
@@ -184,7 +194,8 @@ async function submitForm(
184
194
  model,
185
195
  emailAddress,
186
196
  items,
187
- submitResponse
197
+ submitResponse,
198
+ formMetadata
188
199
  )
189
200
  }
190
201
 
@@ -8,12 +8,12 @@ import {
8
8
 
9
9
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
10
10
  import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
11
+ import { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'
11
12
  import {
12
13
  createPage,
13
- getProxyUrlForLocalDevelopment,
14
14
  isPageController,
15
15
  type PageControllerType
16
- } from '~/src/server/plugins/engine/pageControllers/helpers.js'
16
+ } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
17
17
  import {
18
18
  FileUploadPageController,
19
19
  QuestionPageController,
@@ -0,0 +1,20 @@
1
+ /**
2
+ * In local development environments, we sometimes need to rewrite the
3
+ * CDP upload URL to work with CSP/CORS restrictions.
4
+ * This helper function rewrites localhost URLs to use the sslip.io proxy
5
+ * This is only used when running locally with a development proxy.
6
+ * In non-local environments, this function returns null
7
+ * @param uploadUrl - The original upload URL from CDP
8
+ */
9
+ export function getProxyUrlForLocalDevelopment(
10
+ uploadUrl?: string
11
+ ): string | null {
12
+ if (!uploadUrl?.includes('localhost:7337')) {
13
+ return null
14
+ }
15
+
16
+ return uploadUrl.replace(
17
+ /localhost:7337/g,
18
+ 'uploader.127.0.0.1.sslip.io:7300'
19
+ )
20
+ }
@@ -5,7 +5,7 @@ import {
5
5
  type Page
6
6
  } from '@defra/forms-model'
7
7
 
8
- import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
8
+ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
9
9
  import * as PageControllers from '~/src/server/plugins/engine/pageControllers/index.js'
10
10
 
11
11
  export function isPageController(
@@ -78,24 +78,3 @@ export function createPage(model: FormModel, pageDef: Page) {
78
78
 
79
79
  return controller
80
80
  }
81
-
82
- /**
83
- * In local development environments, we sometimes need to rewrite the
84
- * CDP upload URL to work with CSP/CORS restrictions.
85
- * This helper function rewrites localhost URLs to use the sslip.io proxy
86
- * This is only used when running locally with a development proxy.
87
- * In non-local environments, this function returns null
88
- * @param uploadUrl - The original upload URL from CDP
89
- */
90
- export function getProxyUrlForLocalDevelopment(
91
- uploadUrl?: string
92
- ): string | null {
93
- if (!uploadUrl?.includes('localhost:7337')) {
94
- return null
95
- }
96
-
97
- return uploadUrl.replace(
98
- /localhost:7337/g,
99
- 'uploader.127.0.0.1.sslip.io:7300'
100
- )
101
- }
@@ -17,7 +17,7 @@ import {
17
17
  proceed
18
18
  } from '~/src/server/plugins/engine/helpers.js'
19
19
  import { FormModel } from '~/src/server/plugins/engine/models/index.js'
20
- import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
20
+ import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
21
21
  import { generateUniqueReference } from '~/src/server/plugins/engine/referenceNumbers.js'
22
22
  import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
23
23
  import {
@@ -4,7 +4,7 @@ import { type ResponseObject, type ResponseToolkit } from '@hapi/hapi'
4
4
  import nock from 'nock'
5
5
 
6
6
  import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
7
- import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
7
+ import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
8
8
  import { redirectOrMakeHandler } from '~/src/server/plugins/engine/routes/index.js'
9
9
  import {
10
10
  makeGetHandler,
@@ -19,7 +19,7 @@ import {
19
19
  } from '~/src/server/plugins/engine/models/index.js'
20
20
  import { format } from '~/src/server/plugins/engine/outputFormatters/machine/v1.js'
21
21
  import { getFormSubmissionData } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
22
- import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
22
+ import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
23
23
  import {
24
24
  dispatchHandler,
25
25
  redirectOrMakeHandler
@@ -1,3 +1,6 @@
1
+ import { type FormMetadata } from '@defra/forms-model'
2
+
3
+ import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'
1
4
  import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
2
5
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
3
6
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
@@ -13,6 +16,7 @@ import { sendNotification } from '~/src/server/utils/notify.js'
13
16
  jest.mock('~/src/server/utils/notify')
14
17
  jest.mock('~/src/server/plugins/engine/helpers')
15
18
  jest.mock('~/src/server/plugins/engine/outputFormatters/index')
19
+ jest.mock('~/src/server/plugins/engine/components/helpers')
16
20
 
17
21
  describe('notifyService', () => {
18
22
  const submitResponse = {
@@ -32,7 +36,8 @@ describe('notifyService', () => {
32
36
  const mockRequest: FormRequestPayload = jest.mocked<FormRequestPayload>({
33
37
  path: 'test',
34
38
  logger: {
35
- info: jest.fn()
39
+ info: jest.fn(),
40
+ error: jest.fn()
36
41
  }
37
42
  } as unknown as FormRequestPayload)
38
43
  let model: FormModel
@@ -41,6 +46,7 @@ describe('notifyService', () => {
41
46
 
42
47
  beforeEach(() => {
43
48
  jest.resetAllMocks()
49
+ jest.mocked(escapeMarkdown).mockImplementation((text) => text)
44
50
  })
45
51
 
46
52
  it('creates a subject line for real forms', async () => {
@@ -152,4 +158,153 @@ describe('notifyService', () => {
152
158
  })
153
159
  )
154
160
  })
161
+
162
+ it('calls outputFormatter with all correct arguments', async () => {
163
+ const mockFormatter = jest.fn().mockReturnValue('formatted-output')
164
+ jest.mocked(getFormatter).mockReturnValue(mockFormatter)
165
+
166
+ const formMetadata: FormMetadata = {
167
+ id: 'form-id',
168
+ slug: 'form-slug',
169
+ title: 'Form Title'
170
+ } as FormMetadata
171
+
172
+ const formStatus = {
173
+ isPreview: false,
174
+ state: FormStatus.Live
175
+ }
176
+
177
+ jest.mocked(checkFormStatus).mockReturnValue(formStatus)
178
+
179
+ model = {
180
+ name: 'foobar',
181
+ def: {
182
+ output: {
183
+ audience: 'human',
184
+ version: '1'
185
+ }
186
+ }
187
+ } as FormModel
188
+
189
+ await submit(
190
+ formContext,
191
+ mockRequest,
192
+ model,
193
+ 'test@defra.gov.uk',
194
+ items,
195
+ submitResponse,
196
+ formMetadata
197
+ )
198
+
199
+ expect(getFormatter).toHaveBeenCalledWith('human', '1')
200
+
201
+ expect(mockFormatter).toHaveBeenCalledWith(
202
+ formContext,
203
+ items,
204
+ model,
205
+ submitResponse,
206
+ formStatus,
207
+ formMetadata
208
+ )
209
+
210
+ expect(sendNotificationMock).toHaveBeenCalledWith(
211
+ expect.objectContaining({
212
+ personalisation: {
213
+ subject: 'Form submission: foobar',
214
+ body: 'formatted-output'
215
+ }
216
+ })
217
+ )
218
+ })
219
+
220
+ it('calls outputFormatter without formMetadata when not provided', async () => {
221
+ const mockFormatter = jest.fn().mockReturnValue('formatted-output')
222
+ jest.mocked(getFormatter).mockReturnValue(mockFormatter)
223
+
224
+ const formStatus = {
225
+ isPreview: true,
226
+ state: FormStatus.Draft
227
+ }
228
+
229
+ jest.mocked(checkFormStatus).mockReturnValue(formStatus)
230
+
231
+ model = {
232
+ name: 'foobar',
233
+ def: {
234
+ output: {
235
+ audience: 'machine',
236
+ version: '2'
237
+ }
238
+ }
239
+ } as FormModel
240
+
241
+ await submit(
242
+ formContext,
243
+ mockRequest,
244
+ model,
245
+ 'test@defra.gov.uk',
246
+ items,
247
+ submitResponse
248
+ )
249
+
250
+ expect(getFormatter).toHaveBeenCalledWith('machine', '2')
251
+
252
+ expect(mockFormatter).toHaveBeenCalledWith(
253
+ formContext,
254
+ items,
255
+ model,
256
+ submitResponse,
257
+ formStatus,
258
+ undefined
259
+ )
260
+
261
+ expect(sendNotificationMock).toHaveBeenCalledWith(
262
+ expect.objectContaining({
263
+ personalisation: {
264
+ subject: 'TEST FORM SUBMISSION: foobar',
265
+ body: Buffer.from('formatted-output').toString('base64')
266
+ }
267
+ })
268
+ )
269
+ })
270
+
271
+ it('should handle sendNotification errors and rethrow', async () => {
272
+ const mockFormatter = jest.fn().mockReturnValue('formatted-output')
273
+ jest.mocked(getFormatter).mockReturnValue(mockFormatter)
274
+ jest.mocked(checkFormStatus).mockReturnValue({
275
+ isPreview: false,
276
+ state: FormStatus.Live
277
+ })
278
+
279
+ const testError = new Error('Notification service unavailable')
280
+ sendNotificationMock.mockRejectedValue(testError)
281
+
282
+ model = {
283
+ name: 'foobar',
284
+ def: {
285
+ output: {
286
+ audience: 'human',
287
+ version: '1'
288
+ }
289
+ }
290
+ } as FormModel
291
+
292
+ await expect(
293
+ submit(
294
+ formContext,
295
+ mockRequest,
296
+ model,
297
+ 'test@defra.gov.uk',
298
+ items,
299
+ submitResponse
300
+ )
301
+ ).rejects.toThrow('Notification service unavailable')
302
+
303
+ expect(mockRequest.logger.error).toHaveBeenCalledWith(
304
+ 'Notification service unavailable',
305
+ expect.stringContaining(
306
+ '[emailSendFailed] Error sending notification email'
307
+ )
308
+ )
309
+ })
155
310
  })
@@ -1,7 +1,11 @@
1
- import { getErrorMessage, type SubmitResponsePayload } from '@defra/forms-model'
1
+ import {
2
+ getErrorMessage,
3
+ type FormMetadata,
4
+ type SubmitResponsePayload
5
+ } from '@defra/forms-model'
2
6
 
3
7
  import { config } from '~/src/config/index.js'
4
- import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
8
+ import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'
5
9
  import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
6
10
  import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
7
11
  import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
@@ -12,14 +16,24 @@ import { sendNotification } from '~/src/server/utils/notify.js'
12
16
 
13
17
  const templateId = config.get('notifyTemplateId')
14
18
 
19
+ /**
20
+ * Optional GOV.UK Notify service for consumers who want email notifications
21
+ * Can be disabled by not providing notifyTemplateId in config
22
+ * Can be overridden by providing a custom outputService in the services config
23
+ */
15
24
  export async function submit(
16
25
  context: FormContext,
17
26
  request: FormRequestPayload,
18
27
  model: FormModel,
19
28
  emailAddress: string,
20
29
  items: DetailItem[],
21
- submitResponse: SubmitResponsePayload
30
+ submitResponse: SubmitResponsePayload,
31
+ formMetadata?: FormMetadata
22
32
  ) {
33
+ if (!templateId) {
34
+ return Promise.resolve()
35
+ }
36
+
23
37
  const logTags = ['submit', 'email']
24
38
  const formStatus = checkFormStatus(request.params)
25
39
 
@@ -35,7 +49,14 @@ export async function submit(
35
49
  const outputVersion = model.def.output?.version ?? '1'
36
50
 
37
51
  const outputFormatter = getFormatter(outputAudience, outputVersion)
38
- let body = outputFormatter(context, items, model, submitResponse, formStatus)
52
+ let body = outputFormatter(
53
+ context,
54
+ items,
55
+ model,
56
+ submitResponse,
57
+ formStatus,
58
+ formMetadata
59
+ )
39
60
 
40
61
  // GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload
41
62
  // This is logic specific to Notify, so we include the logic here rather than in the formatter
@@ -0,0 +1,96 @@
1
+ export type {
2
+ CheckAnswers,
3
+ ErrorMessageTemplate,
4
+ ErrorMessageTemplateList,
5
+ FeaturedFormPageViewModel,
6
+ FileState,
7
+ FilterFunction,
8
+ FormAdapterSubmissionMessage,
9
+ FormAdapterSubmissionMessageData,
10
+ FormAdapterSubmissionMessageMeta,
11
+ FormAdapterSubmissionMessageMetaSerialised,
12
+ FormAdapterSubmissionMessagePayload,
13
+ FormAdapterSubmissionService,
14
+ FormContext,
15
+ FormContextRequest,
16
+ FormPageViewModel,
17
+ FormPayload,
18
+ FormPayloadParams,
19
+ FormState,
20
+ FormStateValue,
21
+ FormSubmissionError,
22
+ FormSubmissionState,
23
+ FormValidationResult,
24
+ FormValue,
25
+ GlobalFunction,
26
+ ItemDeletePageViewModel,
27
+ OnRequestCallback,
28
+ PageViewModel,
29
+ PageViewModelBase,
30
+ PluginOptions,
31
+ PreparePageEventRequestOptions,
32
+ RepeatItemState,
33
+ RepeatListState,
34
+ RepeaterSummaryPageViewModel,
35
+ SummaryList,
36
+ SummaryListAction,
37
+ SummaryListRow,
38
+ TempFileState,
39
+ UploadInitiateResponse,
40
+ UploadStatusFileResponse,
41
+ UploadStatusResponse
42
+ } from '~/src/server/plugins/engine/types.js'
43
+
44
+ export {
45
+ FileStatus,
46
+ FormAdapterSubmissionSchemaVersion,
47
+ UploadStatus
48
+ } from '~/src/server/plugins/engine/types.js'
49
+
50
+ export type {
51
+ Detail,
52
+ DetailItem,
53
+ DetailItemBase,
54
+ DetailItemField,
55
+ DetailItemRepeat,
56
+ ExecutableCondition
57
+ } from '~/src/server/plugins/engine/models/types.js'
58
+
59
+ export type {
60
+ BackLink,
61
+ ComponentText,
62
+ ComponentViewModel,
63
+ Content,
64
+ DateInputItem,
65
+ DatePartsState,
66
+ Label,
67
+ ListItem,
68
+ ListItemLabel,
69
+ MonthYearState,
70
+ ViewModel
71
+ } from '~/src/server/plugins/engine/components/types.js'
72
+
73
+ export type { UkAddressState } from '~/src/server/plugins/engine/components/UkAddressField.js'
74
+
75
+ export type {
76
+ FormParams,
77
+ FormQuery,
78
+ FormRequest,
79
+ FormRequestPayload,
80
+ FormRequestPayloadRefs,
81
+ FormRequestRefs
82
+ } from '~/src/server/routes/types.js'
83
+
84
+ export { FormAction, FormStatus } from '~/src/server/routes/types.js'
85
+
86
+ export type {
87
+ FormSubmissionService,
88
+ FormsService,
89
+ OutputService,
90
+ RouteConfig,
91
+ Services
92
+ } from '~/src/server/types.js'
93
+
94
+ export type { RichFormValue } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
95
+
96
+ export * from '~/src/server/plugins/engine/types/schema.js'
@@ -0,0 +1,152 @@
1
+ import { FormStatus } from '@defra/forms-model'
2
+
3
+ import {
4
+ formAdapterSubmissionMessageDataSchema,
5
+ formAdapterSubmissionMessageMetaSchema,
6
+ formAdapterSubmissionMessagePayloadSchema
7
+ } from '~/src/server/plugins/engine/types/schema.js'
8
+ import {
9
+ FormAdapterSubmissionSchemaVersion,
10
+ type FormAdapterSubmissionMessageData,
11
+ type FormAdapterSubmissionMessageMeta,
12
+ type FormAdapterSubmissionMessagePayload
13
+ } from '~/src/server/plugins/engine/types.js'
14
+
15
+ describe('Schema validation', () => {
16
+ describe('formAdapterSubmissionMessageMetaSchema', () => {
17
+ const validMeta: FormAdapterSubmissionMessageMeta = {
18
+ schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
19
+ timestamp: new Date('2025-08-22T18:15:10.785Z'),
20
+ referenceNumber: '576-225-943',
21
+ formName: 'Order a pizza',
22
+ formId: '68a8b0449ab460290c28940a',
23
+ formSlug: 'order-a-pizza',
24
+ status: FormStatus.Live,
25
+ isPreview: false,
26
+ notificationEmail: 'info@example.com'
27
+ }
28
+
29
+ it('should validate valid meta object', () => {
30
+ const { error } =
31
+ formAdapterSubmissionMessageMetaSchema.validate(validMeta)
32
+ expect(error).toBeUndefined()
33
+ })
34
+
35
+ it('should reject invalid schema version', () => {
36
+ const invalidMeta = { ...validMeta, schemaVersion: 'invalid' }
37
+ const { error } =
38
+ formAdapterSubmissionMessageMetaSchema.validate(invalidMeta)
39
+ expect(error).toBeDefined()
40
+ })
41
+
42
+ it('should reject missing required fields', () => {
43
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
44
+ const { timestamp: _, ...metaWithoutTimestamp } = validMeta
45
+ const { error } =
46
+ formAdapterSubmissionMessageMetaSchema.validate(metaWithoutTimestamp)
47
+ expect(error).toBeDefined()
48
+ })
49
+ })
50
+
51
+ describe('formAdapterSubmissionMessageDataSchema', () => {
52
+ const validData: FormAdapterSubmissionMessageData = {
53
+ main: {
54
+ QMwMir: 'Roman Pizza',
55
+ duOEvZ: 'Small',
56
+ DzEODf: ['Mozzarella'],
57
+ juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'],
58
+ YEpypP: 'None',
59
+ JumNVc: 'Joe Bloggs',
60
+ ALNehP: '+441234567890',
61
+ vAqTmg: {
62
+ addressLine1: '1 Anywhere Street',
63
+ town: 'Anywhereville',
64
+ postcode: 'AN1 2WH'
65
+ },
66
+ IbXVGY: {
67
+ day: 22,
68
+ month: 8,
69
+ year: 2025
70
+ },
71
+ HGBWLt: ['Garlic sauce']
72
+ },
73
+ repeaters: {},
74
+ files: {}
75
+ }
76
+
77
+ it('should validate valid data object', () => {
78
+ const { error } =
79
+ formAdapterSubmissionMessageDataSchema.validate(validData)
80
+ expect(error).toBeUndefined()
81
+ })
82
+
83
+ it('should validate empty objects', () => {
84
+ const emptyData = { main: {}, repeaters: {}, files: {} }
85
+ const { error } =
86
+ formAdapterSubmissionMessageDataSchema.validate(emptyData)
87
+ expect(error).toBeUndefined()
88
+ })
89
+ })
90
+
91
+ describe('formAdapterSubmissionMessagePayloadSchema', () => {
92
+ const validPayload: FormAdapterSubmissionMessagePayload = {
93
+ meta: {
94
+ schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
95
+ timestamp: new Date('2025-08-22T18:15:10.785Z'),
96
+ referenceNumber: '576-225-943',
97
+ formName: 'Order a pizza',
98
+ formId: '68a8b0449ab460290c28940a',
99
+ formSlug: 'order-a-pizza',
100
+ status: FormStatus.Live,
101
+ isPreview: false,
102
+ notificationEmail: 'info@example.com'
103
+ },
104
+ data: {
105
+ main: {
106
+ QMwMir: 'Roman Pizza',
107
+ duOEvZ: 'Small',
108
+ DzEODf: ['Mozzarella'],
109
+ juiCfC: ['Pepperoni', 'Sausage', 'Onions', 'Basil'],
110
+ YEpypP: 'None',
111
+ JumNVc: 'Joe Bloggs',
112
+ ALNehP: '+441234567890',
113
+ vAqTmg: {
114
+ addressLine1: '1 Anywhere Street',
115
+ town: 'Anywhereville',
116
+ postcode: 'AN1 2WH'
117
+ },
118
+ IbXVGY: {
119
+ day: 22,
120
+ month: 8,
121
+ year: 2025
122
+ },
123
+ HGBWLt: ['Garlic sauce']
124
+ },
125
+ repeaters: {},
126
+ files: {}
127
+ }
128
+ }
129
+
130
+ it('should validate complete payload', () => {
131
+ const { error } =
132
+ formAdapterSubmissionMessagePayloadSchema.validate(validPayload)
133
+ expect(error).toBeUndefined()
134
+ })
135
+
136
+ it('should reject payload without meta', () => {
137
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
138
+ const { meta: _, ...payloadWithoutMeta } = validPayload
139
+ const { error } =
140
+ formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutMeta)
141
+ expect(error).toBeDefined()
142
+ })
143
+
144
+ it('should reject payload without data', () => {
145
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
146
+ const { data: _, ...payloadWithoutData } = validPayload
147
+ const { error } =
148
+ formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutData)
149
+ expect(error).toBeDefined()
150
+ })
151
+ })
152
+ })