@defra/forms-engine-plugin 4.0.42 → 4.0.44

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 (169) hide show
  1. package/.public/javascripts/shared.min.js +1 -1
  2. package/.public/javascripts/shared.min.js.map +1 -1
  3. package/.public/stylesheets/application.min.css +1 -1
  4. package/.public/stylesheets/application.min.css.map +1 -1
  5. package/.server/client/javascripts/location-map.js +8 -4
  6. package/.server/client/javascripts/location-map.js.map +1 -1
  7. package/.server/client/stylesheets/_payment-field.scss +8 -0
  8. package/.server/client/stylesheets/application.scss +2 -0
  9. package/.server/index.js +3 -1
  10. package/.server/index.js.map +1 -1
  11. package/.server/server/constants.d.ts +1 -0
  12. package/.server/server/constants.js +1 -0
  13. package/.server/server/constants.js.map +1 -1
  14. package/.server/server/forms/payment-test.yaml +42 -0
  15. package/.server/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  16. package/.server/server/plugins/engine/components/FormComponent.d.ts +1 -0
  17. package/.server/server/plugins/engine/components/FormComponent.js +1 -0
  18. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  19. package/.server/server/plugins/engine/components/PaymentField.d.ts +135 -0
  20. package/.server/server/plugins/engine/components/PaymentField.js +228 -0
  21. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -0
  22. package/.server/server/plugins/engine/components/PaymentField.types.d.ts +21 -0
  23. package/.server/server/plugins/engine/components/PaymentField.types.js +2 -0
  24. package/.server/server/plugins/engine/components/PaymentField.types.js.map +1 -0
  25. package/.server/server/plugins/engine/components/UkAddressField.d.ts +1 -1
  26. package/.server/server/plugins/engine/components/UkAddressField.js +3 -1
  27. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  28. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  29. package/.server/server/plugins/engine/components/helpers/components.js +3 -0
  30. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  31. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  32. package/.server/server/plugins/engine/components/index.js +1 -0
  33. package/.server/server/plugins/engine/components/index.js.map +1 -1
  34. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  35. package/.server/server/plugins/engine/configureEnginePlugin.js +4 -2
  36. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  37. package/.server/server/plugins/engine/helpers.d.ts +1 -0
  38. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +3 -0
  39. package/.server/server/plugins/engine/models/SummaryViewModel.js +7 -0
  40. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  41. package/.server/server/plugins/engine/options.js +2 -1
  42. package/.server/server/plugins/engine/options.js.map +1 -1
  43. package/.server/server/plugins/engine/outputFormatters/human/v1.js +34 -1
  44. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  45. package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +22 -0
  46. package/.server/server/plugins/engine/outputFormatters/machine/v2.js +43 -1
  47. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  48. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +29 -8
  49. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  50. package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
  51. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +17 -0
  52. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +173 -51
  53. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  54. package/.server/server/plugins/engine/pageControllers/errors.d.ts +31 -0
  55. package/.server/server/plugins/engine/pageControllers/errors.js +59 -2
  56. package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +27 -0
  58. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +77 -0
  59. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -0
  60. package/.server/server/plugins/engine/plugin.js +10 -5
  61. package/.server/server/plugins/engine/plugin.js.map +1 -1
  62. package/.server/server/plugins/engine/routes/index.js +8 -4
  63. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  64. package/.server/server/plugins/engine/routes/payment-helper.d.ts +14 -0
  65. package/.server/server/plugins/engine/routes/payment-helper.js +41 -0
  66. package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -0
  67. package/.server/server/plugins/engine/routes/payment-helper.test.js +81 -0
  68. package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -0
  69. package/.server/server/plugins/engine/routes/payment.d.ts +8 -0
  70. package/.server/server/plugins/engine/routes/payment.js +140 -0
  71. package/.server/server/plugins/engine/routes/payment.js.map +1 -0
  72. package/.server/server/plugins/engine/routes/payment.test.js +187 -0
  73. package/.server/server/plugins/engine/routes/payment.test.js.map +1 -0
  74. package/.server/server/plugins/engine/services/localFormsService.js +6 -0
  75. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  76. package/.server/server/plugins/engine/types/schema.js +7 -0
  77. package/.server/server/plugins/engine/types/schema.js.map +1 -1
  78. package/.server/server/plugins/engine/types.d.ts +20 -1
  79. package/.server/server/plugins/engine/types.js +4 -0
  80. package/.server/server/plugins/engine/types.js.map +1 -1
  81. package/.server/server/plugins/engine/validationHelpers.d.ts +1 -1
  82. package/.server/server/plugins/engine/validationHelpers.js.map +1 -1
  83. package/.server/server/plugins/engine/views/components/paymentfield.html +42 -0
  84. package/.server/server/plugins/engine/views/index.html +9 -1
  85. package/.server/server/plugins/engine/views/partials/form.html +20 -5
  86. package/.server/server/plugins/engine/views/summary.html +17 -1
  87. package/.server/server/plugins/map/routes/get-os-token.d.ts +6 -0
  88. package/.server/server/plugins/map/routes/get-os-token.js +41 -0
  89. package/.server/server/plugins/map/routes/get-os-token.js.map +1 -0
  90. package/.server/server/plugins/map/routes/get-os-token.test.js +49 -0
  91. package/.server/server/plugins/map/routes/get-os-token.test.js.map +1 -0
  92. package/.server/server/plugins/map/routes/index.d.ts +1 -11
  93. package/.server/server/plugins/map/routes/index.js +60 -16
  94. package/.server/server/plugins/map/routes/index.js.map +1 -1
  95. package/.server/server/plugins/map/types.d.ts +1 -0
  96. package/.server/server/plugins/map/types.js +1 -0
  97. package/.server/server/plugins/map/types.js.map +1 -1
  98. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  99. package/.server/server/plugins/payment/helper.d.ts +30 -0
  100. package/.server/server/plugins/payment/helper.js +49 -0
  101. package/.server/server/plugins/payment/helper.js.map +1 -0
  102. package/.server/server/plugins/payment/helper.test.js +37 -0
  103. package/.server/server/plugins/payment/helper.test.js.map +1 -0
  104. package/.server/server/plugins/payment/service.d.ts +40 -0
  105. package/.server/server/plugins/payment/service.js +129 -0
  106. package/.server/server/plugins/payment/service.js.map +1 -0
  107. package/.server/server/plugins/payment/service.test.js +162 -0
  108. package/.server/server/plugins/payment/service.test.js.map +1 -0
  109. package/.server/server/plugins/payment/types.d.ts +172 -0
  110. package/.server/server/plugins/payment/types.js +78 -0
  111. package/.server/server/plugins/payment/types.js.map +1 -0
  112. package/.server/server/types.d.ts +3 -0
  113. package/.server/server/types.js.map +1 -1
  114. package/.server/typings/hapi/index.d.js.map +1 -1
  115. package/README.md +12 -9
  116. package/package.json +2 -2
  117. package/src/client/javascripts/location-map.js +12 -4
  118. package/src/client/stylesheets/_payment-field.scss +8 -0
  119. package/src/client/stylesheets/application.scss +2 -0
  120. package/src/index.ts +5 -1
  121. package/src/server/constants.js +1 -0
  122. package/src/server/forms/payment-test.yaml +42 -0
  123. package/src/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
  124. package/src/server/plugins/engine/components/FormComponent.ts +1 -0
  125. package/src/server/plugins/engine/components/PaymentField.test.ts +611 -0
  126. package/src/server/plugins/engine/components/PaymentField.ts +367 -0
  127. package/src/server/plugins/engine/components/PaymentField.types.ts +21 -0
  128. package/src/server/plugins/engine/components/UkAddressField.ts +2 -1
  129. package/src/server/plugins/engine/components/helpers/components.ts +5 -0
  130. package/src/server/plugins/engine/components/index.ts +1 -0
  131. package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
  132. package/src/server/plugins/engine/models/SummaryViewModel.ts +8 -0
  133. package/src/server/plugins/engine/options.js +2 -1
  134. package/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +147 -0
  135. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +105 -103
  136. package/src/server/plugins/engine/outputFormatters/human/v1.ts +61 -2
  137. package/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +115 -0
  138. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +60 -1
  139. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -6
  140. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +247 -72
  141. package/src/server/plugins/engine/pageControllers/errors.test.ts +13 -1
  142. package/src/server/plugins/engine/pageControllers/errors.ts +79 -4
  143. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +299 -0
  144. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +110 -0
  145. package/src/server/plugins/engine/plugin.ts +17 -10
  146. package/src/server/plugins/engine/routes/index.ts +17 -16
  147. package/src/server/plugins/engine/routes/payment-helper.js +39 -0
  148. package/src/server/plugins/engine/routes/payment-helper.test.js +90 -0
  149. package/src/server/plugins/engine/routes/payment.js +151 -0
  150. package/src/server/plugins/engine/routes/payment.test.js +180 -0
  151. package/src/server/plugins/engine/services/localFormsService.js +7 -0
  152. package/src/server/plugins/engine/types/schema.ts +9 -0
  153. package/src/server/plugins/engine/types.ts +25 -1
  154. package/src/server/plugins/engine/validationHelpers.ts +1 -1
  155. package/src/server/plugins/engine/views/components/paymentfield.html +42 -0
  156. package/src/server/plugins/engine/views/index.html +9 -1
  157. package/src/server/plugins/engine/views/partials/form.html +20 -5
  158. package/src/server/plugins/engine/views/summary.html +17 -1
  159. package/src/server/plugins/map/routes/get-os-token.js +41 -0
  160. package/src/server/plugins/map/routes/get-os-token.test.js +55 -0
  161. package/src/server/plugins/map/routes/index.js +70 -24
  162. package/src/server/plugins/map/types.js +1 -0
  163. package/src/server/plugins/payment/helper.js +56 -0
  164. package/src/server/plugins/payment/helper.test.js +52 -0
  165. package/src/server/plugins/payment/service.js +171 -0
  166. package/src/server/plugins/payment/service.test.js +205 -0
  167. package/src/server/plugins/payment/types.js +77 -0
  168. package/src/server/types.ts +3 -0
  169. package/src/typings/hapi/index.d.ts +1 -0
@@ -0,0 +1,367 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ import {
4
+ type FormMetadata,
5
+ type PaymentFieldComponent
6
+ } from '@defra/forms-model'
7
+ import { StatusCodes } from 'http-status-codes'
8
+ import joi, { type ObjectSchema } from 'joi'
9
+
10
+ import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
11
+ 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
+ PaymentErrorTypes,
15
+ PaymentPreAuthError,
16
+ PaymentSubmissionError
17
+ } from '~/src/server/plugins/engine/pageControllers/errors.js'
18
+ import {
19
+ type AnyFormRequest,
20
+ type FormContext,
21
+ type FormRequestPayload,
22
+ type FormResponseToolkit
23
+ } from '~/src/server/plugins/engine/types/index.js'
24
+ import {
25
+ type ErrorMessageTemplateList,
26
+ type FormPayload,
27
+ type FormState,
28
+ type FormStateValue,
29
+ type FormSubmissionError,
30
+ type FormSubmissionState
31
+ } from '~/src/server/plugins/engine/types.js'
32
+ import { createPaymentService } from '~/src/server/plugins/payment/helper.js'
33
+
34
+ export class PaymentField extends FormComponent {
35
+ declare options: PaymentFieldComponent['options']
36
+ declare formSchema: ObjectSchema
37
+ declare stateSchema: ObjectSchema
38
+ isAppendageStateSingleObject = true
39
+
40
+ constructor(
41
+ def: PaymentFieldComponent,
42
+ props: ConstructorParameters<typeof FormComponent>[1]
43
+ ) {
44
+ super(def, props)
45
+
46
+ this.options = def.options
47
+
48
+ const paymentStateSchema = joi
49
+ .object({
50
+ paymentId: joi.string().required(),
51
+ reference: joi.string().required(),
52
+ amount: joi.number().required(),
53
+ description: joi.string().required(),
54
+ uuid: joi.string().uuid().required(),
55
+ formId: joi.string().required(),
56
+ isLivePayment: joi.boolean().required(),
57
+ preAuth: joi
58
+ .object({
59
+ status: joi
60
+ .string()
61
+ .valid('success', 'failed', 'started')
62
+ .required(),
63
+ createdAt: joi.string().isoDate().required()
64
+ })
65
+ .required()
66
+ })
67
+ .unknown(true)
68
+ .label(this.label)
69
+
70
+ this.formSchema = paymentStateSchema
71
+ // 'required()' forces the payment page to be invalid until we have valid payment state
72
+ // i.e. the user will automatically be directed back to the payment page
73
+ // if they attempt to access future pages when no payment entered yet
74
+ this.stateSchema = paymentStateSchema.required()
75
+ }
76
+
77
+ /**
78
+ * Gets the PaymentState from form submission state
79
+ */
80
+ getPaymentStateFromState(
81
+ state: FormSubmissionState
82
+ ): PaymentState | undefined {
83
+ const value = state[this.name]
84
+ return this.isPaymentState(value) ? value : undefined
85
+ }
86
+
87
+ getDisplayStringFromState(state: FormSubmissionState): string {
88
+ const value = this.getPaymentStateFromState(state)
89
+
90
+ if (!value) {
91
+ return ''
92
+ }
93
+
94
+ return `£${value.amount.toFixed(2)} - ${value.description}`
95
+ }
96
+
97
+ getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
98
+ const viewModel = super.getViewModel(payload, errors)
99
+
100
+ // Payload is pre-populated from state if a payment has already been made
101
+ const paymentState = this.isPaymentState(payload[this.name] as unknown)
102
+ ? (payload[this.name] as unknown as PaymentState)
103
+ : undefined
104
+
105
+ // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.
106
+ const amount = paymentState?.amount ?? this.options.amount
107
+
108
+ const formattedAmount = amount.toFixed(2)
109
+
110
+ return {
111
+ ...viewModel,
112
+ amount: formattedAmount,
113
+ description: this.options.description,
114
+ paymentState
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Type guard to check if value is PaymentState
120
+ */
121
+ isPaymentState(value: unknown): value is PaymentState {
122
+ return PaymentField.isPaymentState(value)
123
+ }
124
+
125
+ /**
126
+ * Static type guard to check if value is PaymentState
127
+ */
128
+ static isPaymentState(value: unknown): value is PaymentState {
129
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
130
+ return false
131
+ }
132
+
133
+ const state = value as PaymentState
134
+ return (
135
+ typeof state.paymentId === 'string' &&
136
+ typeof state.amount === 'number' &&
137
+ typeof state.description === 'string'
138
+ )
139
+ }
140
+
141
+ /**
142
+ * Override base isState to validate PaymentState
143
+ */
144
+ isState(value?: FormStateValue | FormState): value is FormState {
145
+ return this.isPaymentState(value)
146
+ }
147
+
148
+ getFormValue(value?: FormStateValue | FormState) {
149
+ return this.isPaymentState(value)
150
+ ? (value as unknown as NonNullable<FormStateValue>)
151
+ : undefined
152
+ }
153
+
154
+ getContextValueFromState(state: FormSubmissionState) {
155
+ return this.isPaymentState(state)
156
+ ? `Reference: ${state.reference}\nAmount: ${state.amount.toFixed(2)}`
157
+ : ''
158
+ }
159
+
160
+ /**
161
+ * For error preview page that shows all possible errors on a component
162
+ */
163
+ getAllPossibleErrors(): ErrorMessageTemplateList {
164
+ return PaymentField.getAllPossibleErrors()
165
+ }
166
+
167
+ /**
168
+ * Static version of getAllPossibleErrors that doesn't require a component instance.
169
+ */
170
+ static getAllPossibleErrors(): ErrorMessageTemplateList {
171
+ return {
172
+ baseErrors: [
173
+ {
174
+ type: 'paymentRequired',
175
+ template: 'Complete the payment to continue'
176
+ }
177
+ ],
178
+ advancedSettingsErrors: []
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Dispatcher for external redirect to GOV.UK Pay
184
+ */
185
+ static async dispatcher(
186
+ request: FormRequestPayload,
187
+ h: FormResponseToolkit,
188
+ args: PaymentDispatcherArgs
189
+ ): Promise<unknown> {
190
+ const { options, name: componentName } = args.component
191
+ const { model } = args.controller
192
+
193
+ const state = await args.controller.getState(request)
194
+ const { baseUrl } = getPluginOptions(request.server)
195
+ const summaryUrl = `${baseUrl}/${model.basePath}/summary`
196
+
197
+ const existingPaymentState = state[componentName]
198
+ if (
199
+ PaymentField.isPaymentState(existingPaymentState) &&
200
+ existingPaymentState.preAuth?.status === 'success'
201
+ ) {
202
+ return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)
203
+ }
204
+
205
+ const isLivePayment = args.isLive && !args.isPreview
206
+ const formId = args.controller.model.formId
207
+ const paymentService = createPaymentService(isLivePayment, formId)
208
+
209
+ const uuid = randomUUID()
210
+
211
+ const reference = state.$$__referenceNumber as string
212
+ const amount = options.amount
213
+
214
+ const description = options.description
215
+
216
+ const slug = `/${model.basePath}`
217
+
218
+ const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`
219
+ const paymentPageUrl = args.sourceUrl
220
+
221
+ const amountInPence = Math.round(amount * 100)
222
+ const payment = await paymentService.createPayment(
223
+ amountInPence,
224
+ description,
225
+ payCallbackUrl,
226
+ reference,
227
+ { formId, slug }
228
+ )
229
+
230
+ const sessionData: PaymentSessionData = {
231
+ uuid,
232
+ formId,
233
+ reference,
234
+ amount,
235
+ description,
236
+ paymentId: payment.paymentId,
237
+ componentName,
238
+ returnUrl: summaryUrl,
239
+ failureUrl: paymentPageUrl,
240
+ isLivePayment
241
+ }
242
+
243
+ request.yar.set(`payment-${uuid}`, sessionData)
244
+
245
+ return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)
246
+ }
247
+
248
+ /**
249
+ * Called on form submission to capture the payment
250
+ * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment
251
+ */
252
+ async onSubmit(
253
+ request: FormRequestPayload,
254
+ _metadata: FormMetadata,
255
+ context: FormContext
256
+ ): Promise<void> {
257
+ const paymentState = this.getPaymentStateFromState(context.state)
258
+
259
+ if (!paymentState) {
260
+ throw new PaymentPreAuthError(
261
+ this,
262
+ 'Complete the payment to continue',
263
+ true,
264
+ PaymentErrorTypes.PaymentIncomplete
265
+ )
266
+ }
267
+
268
+ if (paymentState.capture?.status === 'success') {
269
+ return
270
+ }
271
+
272
+ const { paymentId, isLivePayment, formId } = paymentState
273
+ const paymentService = createPaymentService(isLivePayment, formId)
274
+
275
+ /**
276
+ * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
277
+ */
278
+ const status = await paymentService.getPaymentStatus(paymentId)
279
+
280
+ PaymentSubmissionError.checkPaymentAmount(
281
+ status.amount,
282
+ this.options.amount,
283
+ this
284
+ )
285
+
286
+ if (status.state.status === 'success') {
287
+ await this.markPaymentCaptured(request, paymentState)
288
+ return
289
+ }
290
+
291
+ if (status.state.status !== 'capturable') {
292
+ throw new PaymentPreAuthError(
293
+ this,
294
+ 'Your payment authorisation has expired. Please add your payment details again.',
295
+ true,
296
+ PaymentErrorTypes.PaymentExpired
297
+ )
298
+ }
299
+
300
+ const captured = await paymentService.capturePayment(paymentId)
301
+
302
+ if (!captured) {
303
+ throw new PaymentPreAuthError(
304
+ this,
305
+ 'There was a problem and your form was not submitted. Try submitting the form again.',
306
+ false
307
+ )
308
+ }
309
+
310
+ await this.markPaymentCaptured(request, paymentState)
311
+ }
312
+
313
+ /**
314
+ * Updates payment state to mark capture as successful
315
+ * This ensures we don't try to re-capture on submission retry
316
+ */
317
+ private async markPaymentCaptured(
318
+ request: FormRequestPayload,
319
+ paymentState: PaymentState
320
+ ): Promise<void> {
321
+ const updatedState: PaymentState = {
322
+ ...paymentState,
323
+ capture: {
324
+ status: 'success',
325
+ createdAt: new Date().toISOString()
326
+ }
327
+ }
328
+
329
+ if (this.page) {
330
+ const currentState = await this.page.getState(request)
331
+ await this.page.mergeState(request, currentState, {
332
+ [this.name]: updatedState
333
+ })
334
+ }
335
+ }
336
+ }
337
+
338
+ export interface PaymentDispatcherArgs {
339
+ controller: {
340
+ model: {
341
+ formId: string
342
+ basePath: string
343
+ name: string
344
+ }
345
+ getState: (request: AnyFormRequest) => Promise<FormSubmissionState>
346
+ }
347
+ component: PaymentField
348
+ sourceUrl: string
349
+ isLive: boolean
350
+ isPreview: boolean
351
+ }
352
+
353
+ /**
354
+ * Session data stored when dispatching to GOV.UK Pay
355
+ */
356
+ export interface PaymentSessionData {
357
+ uuid: string
358
+ formId: string
359
+ reference: string
360
+ amount: number
361
+ description: string
362
+ paymentId: string
363
+ componentName: string
364
+ returnUrl: string
365
+ failureUrl: string
366
+ isLivePayment: boolean
367
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Component state stored in session after pre-auth
3
+ */
4
+ export interface PaymentState {
5
+ paymentId: string
6
+ reference: string
7
+ amount: number
8
+ description: string
9
+ uuid: string
10
+ formId: string
11
+ isLivePayment: boolean
12
+ payerEmail?: string
13
+ capture?: {
14
+ status: 'success' | 'failed'
15
+ createdAt: string
16
+ }
17
+ preAuth?: {
18
+ status: 'success' | 'failed' | 'started'
19
+ createdAt: string
20
+ }
21
+ }
@@ -284,7 +284,8 @@ export class UkAddressField extends FormComponent {
284
284
  )
285
285
  }
286
286
 
287
- static dispatcher(
287
+ // eslint-disable-next-line @typescript-eslint/require-await
288
+ static async dispatcher(
288
289
  request: FormRequestPayload,
289
290
  h: FormResponseToolkit,
290
291
  args: PostcodeLookupExternalArgs
@@ -35,6 +35,7 @@ export type Field = InstanceType<
35
35
  | typeof Components.UkAddressField
36
36
  | typeof Components.FileUploadField
37
37
  | typeof Components.HiddenField
38
+ | typeof Components.PaymentField
38
39
  >
39
40
 
40
41
  // Guidance component instances only
@@ -191,6 +192,10 @@ export function createComponent(
191
192
  case ComponentType.HiddenField:
192
193
  component = new Components.HiddenField(def, options)
193
194
  break
195
+
196
+ case ComponentType.PaymentField:
197
+ component = new Components.PaymentField(def, options)
198
+ break
194
199
  }
195
200
 
196
201
  if (typeof component === 'undefined') {
@@ -29,3 +29,4 @@ export { OsGridRefField } from '~/src/server/plugins/engine/components/OsGridRef
29
29
  export { NationalGridFieldNumberField } from '~/src/server/plugins/engine/components/NationalGridFieldNumberField.js'
30
30
  export { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
31
31
  export { HiddenField } from '~/src/server/plugins/engine/components/HiddenField.js'
32
+ export { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
@@ -22,7 +22,8 @@ export const configureEnginePlugin = async (
22
22
  preparePageEventRequestOptions,
23
23
  onRequest,
24
24
  saveAndExit,
25
- ordnanceSurveyApiKey
25
+ ordnanceSurveyApiKey,
26
+ ordnanceSurveyApiSecret
26
27
  }: RouteConfig = {},
27
28
  cache?: CacheService
28
29
  ): Promise<{
@@ -65,7 +66,8 @@ export const configureEnginePlugin = async (
65
66
  onRequest,
66
67
  baseUrl: 'http://localhost:3009', // always runs locally
67
68
  saveAndExit,
68
- ordnanceSurveyApiKey
69
+ ordnanceSurveyApiKey,
70
+ ordnanceSurveyApiSecret
69
71
  }
70
72
  }
71
73
  }
@@ -1,5 +1,7 @@
1
1
  import { SchemaVersion, type Section } from '@defra/forms-model'
2
2
 
3
+ import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
4
+ import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
3
5
  import {
4
6
  getAnswer,
5
7
  type Field
@@ -52,6 +54,8 @@ export class SummaryViewModel {
52
54
  hasMissingNotificationEmail?: boolean
53
55
  components?: ComponentViewModel[]
54
56
  allowSaveAndExit = false
57
+ paymentState?: PaymentState
58
+ paymentDetails?: CheckAnswers
55
59
 
56
60
  constructor(
57
61
  request: FormContextRequest,
@@ -144,6 +148,10 @@ export class SummaryViewModel {
144
148
  )
145
149
  } else {
146
150
  for (const field of collection.fields) {
151
+ // PaymentField is rendered in its own section, skip it here
152
+ if (field instanceof PaymentField) {
153
+ continue
154
+ }
147
155
  items.push(ItemField(page, state, field, { path, errors }))
148
156
  }
149
157
  }
@@ -26,7 +26,8 @@ const pluginRegistrationOptionsSchema = Joi.object({
26
26
  onRequest: Joi.function().optional(),
27
27
  baseUrl: Joi.string().uri().required(),
28
28
  saveAndExit: Joi.function().optional(),
29
- ordnanceSurveyApiKey: Joi.string().optional()
29
+ ordnanceSurveyApiKey: Joi.string().optional(),
30
+ ordnanceSurveyApiSecret: Joi.string().optional()
30
31
  })
31
32
 
32
33
  /**
@@ -0,0 +1,147 @@
1
+ import { format as dateFormat } from 'date-fns'
2
+ import { outdent } from 'outdent'
3
+
4
+ import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
5
+ import { FormModel } from '~/src/server/plugins/engine/models/index.js'
6
+ import { format } from '~/src/server/plugins/engine/outputFormatters/human/v1.js'
7
+ import {
8
+ SummaryPageController,
9
+ getFormSubmissionData
10
+ } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
11
+ import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
12
+ import { type FormSubmissionState } from '~/src/server/plugins/engine/types.js'
13
+ import { FormStatus } from '~/src/server/routes/types.js'
14
+ import definitionPayment from '~/test/form/definitions/payment.js'
15
+
16
+ describe('v1 human formatter', () => {
17
+ describe('Payment', () => {
18
+ const modelPayment = new FormModel(definitionPayment, {
19
+ basePath: 'test'
20
+ })
21
+
22
+ const submitResponse = {
23
+ message: 'Submit completed',
24
+ result: {
25
+ files: {
26
+ main: '00000000-0000-0000-0000-000000000000',
27
+ repeaters: {
28
+ pizza: '11111111-1111-1111-1111-111111111111'
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ const statePayment = {
35
+ $$__referenceNumber: 'foobar',
36
+ licenceLength: 365,
37
+ fullName: 'John Smith',
38
+ paymentField: {
39
+ paymentId: 'payment-id',
40
+ reference: 'payment-ref',
41
+ amount: 250,
42
+ description: 'Payment desc',
43
+ uuid: 'uuid',
44
+ formId: 'form-id',
45
+ isLivePayment: false,
46
+ preAuth: {
47
+ status: 'success',
48
+ createdAt: '2026-01-02T11:02:04+0000'
49
+ }
50
+ } as PaymentState
51
+ }
52
+
53
+ const pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
54
+
55
+ const requestPayment = buildFormContextRequest({
56
+ method: 'get',
57
+ url: pageUrl,
58
+ path: pageUrl.pathname,
59
+ params: {
60
+ path: 'summary',
61
+ slug: 'payment'
62
+ },
63
+ query: {},
64
+ app: { model: modelPayment }
65
+ })
66
+
67
+ const pageDefPayment = definitionPayment.pages[2]
68
+
69
+ const controllerPayment = new SummaryPageController(
70
+ modelPayment,
71
+ pageDefPayment
72
+ )
73
+
74
+ const contextPayment = modelPayment.getFormContext(
75
+ requestPayment,
76
+ statePayment as unknown as FormSubmissionState
77
+ )
78
+ const summaryViewModelPayment = controllerPayment.getSummaryViewModel(
79
+ requestPayment,
80
+ contextPayment
81
+ )
82
+
83
+ const itemsPayment = getFormSubmissionData(
84
+ summaryViewModelPayment.context,
85
+ summaryViewModelPayment.details
86
+ )
87
+
88
+ it('should add payment details', () => {
89
+ const body = format(
90
+ contextPayment,
91
+ itemsPayment,
92
+ modelPayment,
93
+ submitResponse,
94
+ {
95
+ state: FormStatus.Draft,
96
+ isPreview: true
97
+ }
98
+ )
99
+
100
+ const dateNow = new Date()
101
+
102
+ expect(body).toContain(
103
+ outdent`
104
+ ${definitionPayment.name} form received at ${dateFormat(dateNow, 'h:mmaaa')} on ${dateFormat(dateNow, 'd MMMM yyyy')}.
105
+
106
+ ---
107
+
108
+ ## Which fishing licence do you want to get?
109
+
110
+ 12 months \\(365\\)
111
+
112
+ ---
113
+
114
+ ## What\\'s your name?
115
+
116
+ John Smith
117
+
118
+ ---
119
+
120
+ [Download main form \\(CSV\\)](https://forms-designer/file-download/00000000-0000-0000-0000-000000000000)
121
+
122
+ ---
123
+
124
+ # Your payment of £250.00 was successful
125
+
126
+ ## Payment for
127
+
128
+ Payment desc
129
+
130
+ ---
131
+
132
+ ## Total amount
133
+
134
+ £250.00
135
+
136
+ ---
137
+
138
+ ## Date of payment
139
+
140
+ 2 January 2026 11:02am
141
+
142
+ ---
143
+ `
144
+ )
145
+ })
146
+ })
147
+ })