@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.
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.public/stylesheets/application.min.css +1 -1
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/javascripts/location-map.js +8 -4
- package/.server/client/javascripts/location-map.js.map +1 -1
- package/.server/client/stylesheets/_payment-field.scss +8 -0
- package/.server/client/stylesheets/application.scss +2 -0
- package/.server/index.js +3 -1
- package/.server/index.js.map +1 -1
- package/.server/server/constants.d.ts +1 -0
- package/.server/server/constants.js +1 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/forms/payment-test.yaml +42 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
- package/.server/server/plugins/engine/components/FormComponent.d.ts +1 -0
- package/.server/server/plugins/engine/components/FormComponent.js +1 -0
- package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
- package/.server/server/plugins/engine/components/PaymentField.d.ts +135 -0
- package/.server/server/plugins/engine/components/PaymentField.js +228 -0
- package/.server/server/plugins/engine/components/PaymentField.js.map +1 -0
- package/.server/server/plugins/engine/components/PaymentField.types.d.ts +21 -0
- package/.server/server/plugins/engine/components/PaymentField.types.js +2 -0
- package/.server/server/plugins/engine/components/PaymentField.types.js.map +1 -0
- package/.server/server/plugins/engine/components/UkAddressField.d.ts +1 -1
- package/.server/server/plugins/engine/components/UkAddressField.js +3 -1
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +3 -0
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/index.d.ts +1 -0
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +4 -2
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +1 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +3 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js +7 -0
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/options.js +2 -1
- package/.server/server/plugins/engine/options.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js +34 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.d.ts +22 -0
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js +43 -1
- package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +29 -8
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StartPageController.d.ts +2 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +17 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +173 -51
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/errors.d.ts +31 -0
- package/.server/server/plugins/engine/pageControllers/errors.js +59 -2
- package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.d.ts +27 -0
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js +77 -0
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -0
- package/.server/server/plugins/engine/plugin.js +10 -5
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/routes/index.js +8 -4
- package/.server/server/plugins/engine/routes/index.js.map +1 -1
- package/.server/server/plugins/engine/routes/payment-helper.d.ts +14 -0
- package/.server/server/plugins/engine/routes/payment-helper.js +41 -0
- package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment-helper.test.js +81 -0
- package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment.d.ts +8 -0
- package/.server/server/plugins/engine/routes/payment.js +140 -0
- package/.server/server/plugins/engine/routes/payment.js.map +1 -0
- package/.server/server/plugins/engine/routes/payment.test.js +187 -0
- package/.server/server/plugins/engine/routes/payment.test.js.map +1 -0
- package/.server/server/plugins/engine/services/localFormsService.js +6 -0
- package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
- package/.server/server/plugins/engine/types/schema.js +7 -0
- package/.server/server/plugins/engine/types/schema.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +20 -1
- package/.server/server/plugins/engine/types.js +4 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/validationHelpers.d.ts +1 -1
- package/.server/server/plugins/engine/validationHelpers.js.map +1 -1
- package/.server/server/plugins/engine/views/components/paymentfield.html +42 -0
- package/.server/server/plugins/engine/views/index.html +9 -1
- package/.server/server/plugins/engine/views/partials/form.html +20 -5
- package/.server/server/plugins/engine/views/summary.html +17 -1
- package/.server/server/plugins/map/routes/get-os-token.d.ts +6 -0
- package/.server/server/plugins/map/routes/get-os-token.js +41 -0
- package/.server/server/plugins/map/routes/get-os-token.js.map +1 -0
- package/.server/server/plugins/map/routes/get-os-token.test.js +49 -0
- package/.server/server/plugins/map/routes/get-os-token.test.js.map +1 -0
- package/.server/server/plugins/map/routes/index.d.ts +1 -11
- package/.server/server/plugins/map/routes/index.js +60 -16
- package/.server/server/plugins/map/routes/index.js.map +1 -1
- package/.server/server/plugins/map/types.d.ts +1 -0
- package/.server/server/plugins/map/types.js +1 -0
- package/.server/server/plugins/map/types.js.map +1 -1
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/plugins/payment/helper.d.ts +30 -0
- package/.server/server/plugins/payment/helper.js +49 -0
- package/.server/server/plugins/payment/helper.js.map +1 -0
- package/.server/server/plugins/payment/helper.test.js +37 -0
- package/.server/server/plugins/payment/helper.test.js.map +1 -0
- package/.server/server/plugins/payment/service.d.ts +40 -0
- package/.server/server/plugins/payment/service.js +129 -0
- package/.server/server/plugins/payment/service.js.map +1 -0
- package/.server/server/plugins/payment/service.test.js +162 -0
- package/.server/server/plugins/payment/service.test.js.map +1 -0
- package/.server/server/plugins/payment/types.d.ts +172 -0
- package/.server/server/plugins/payment/types.js +78 -0
- package/.server/server/plugins/payment/types.js.map +1 -0
- package/.server/server/types.d.ts +3 -0
- package/.server/server/types.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/README.md +12 -9
- package/package.json +2 -2
- package/src/client/javascripts/location-map.js +12 -4
- package/src/client/stylesheets/_payment-field.scss +8 -0
- package/src/client/stylesheets/application.scss +2 -0
- package/src/index.ts +5 -1
- package/src/server/constants.js +1 -0
- package/src/server/forms/payment-test.yaml +42 -0
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +14 -0
- package/src/server/plugins/engine/components/FormComponent.ts +1 -0
- package/src/server/plugins/engine/components/PaymentField.test.ts +611 -0
- package/src/server/plugins/engine/components/PaymentField.ts +367 -0
- package/src/server/plugins/engine/components/PaymentField.types.ts +21 -0
- package/src/server/plugins/engine/components/UkAddressField.ts +2 -1
- package/src/server/plugins/engine/components/helpers/components.ts +5 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
- package/src/server/plugins/engine/models/SummaryViewModel.ts +8 -0
- package/src/server/plugins/engine/options.js +2 -1
- package/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +147 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +105 -103
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +61 -2
- package/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +115 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +60 -1
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -6
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +247 -72
- package/src/server/plugins/engine/pageControllers/errors.test.ts +13 -1
- package/src/server/plugins/engine/pageControllers/errors.ts +79 -4
- package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +299 -0
- package/src/server/plugins/engine/pageControllers/helpers/submission.ts +110 -0
- package/src/server/plugins/engine/plugin.ts +17 -10
- package/src/server/plugins/engine/routes/index.ts +17 -16
- package/src/server/plugins/engine/routes/payment-helper.js +39 -0
- package/src/server/plugins/engine/routes/payment-helper.test.js +90 -0
- package/src/server/plugins/engine/routes/payment.js +151 -0
- package/src/server/plugins/engine/routes/payment.test.js +180 -0
- package/src/server/plugins/engine/services/localFormsService.js +7 -0
- package/src/server/plugins/engine/types/schema.ts +9 -0
- package/src/server/plugins/engine/types.ts +25 -1
- package/src/server/plugins/engine/validationHelpers.ts +1 -1
- package/src/server/plugins/engine/views/components/paymentfield.html +42 -0
- package/src/server/plugins/engine/views/index.html +9 -1
- package/src/server/plugins/engine/views/partials/form.html +20 -5
- package/src/server/plugins/engine/views/summary.html +17 -1
- package/src/server/plugins/map/routes/get-os-token.js +41 -0
- package/src/server/plugins/map/routes/get-os-token.test.js +55 -0
- package/src/server/plugins/map/routes/index.js +70 -24
- package/src/server/plugins/map/types.js +1 -0
- package/src/server/plugins/payment/helper.js +56 -0
- package/src/server/plugins/payment/helper.test.js +52 -0
- package/src/server/plugins/payment/service.js +171 -0
- package/src/server/plugins/payment/service.test.js +205 -0
- package/src/server/plugins/payment/types.js +77 -0
- package/src/server/types.ts +3 -0
- package/src/typings/hapi/index.d.ts +1 -0
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
import Boom from '@hapi/boom'
|
|
8
8
|
import { type RouteOptions } from '@hapi/hapi'
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
COMPONENT_STATE_ERROR,
|
|
12
|
+
PAYMENT_EXPIRED_NOTIFICATION
|
|
13
|
+
} from '~/src/server/constants.js'
|
|
11
14
|
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
12
|
-
import {
|
|
15
|
+
import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
|
|
13
16
|
import {
|
|
14
17
|
checkEmailAddressForLiveFormSubmission,
|
|
15
18
|
checkFormStatus,
|
|
@@ -22,15 +25,30 @@ import {
|
|
|
22
25
|
} from '~/src/server/plugins/engine/models/index.js'
|
|
23
26
|
import {
|
|
24
27
|
type Detail,
|
|
25
|
-
type DetailItem
|
|
28
|
+
type DetailItem,
|
|
29
|
+
type DetailItemField
|
|
26
30
|
} from '~/src/server/plugins/engine/models/types.js'
|
|
27
31
|
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
|
|
28
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
InvalidComponentStateError,
|
|
34
|
+
PaymentErrorTypes,
|
|
35
|
+
PaymentPreAuthError,
|
|
36
|
+
PaymentSubmissionError
|
|
37
|
+
} from '~/src/server/plugins/engine/pageControllers/errors.js'
|
|
38
|
+
import {
|
|
39
|
+
buildMainRecords,
|
|
40
|
+
buildRepeaterRecords
|
|
41
|
+
} from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'
|
|
29
42
|
import {
|
|
30
43
|
type FormConfirmationState,
|
|
31
44
|
type FormContext,
|
|
32
45
|
type FormContextRequest
|
|
33
46
|
} from '~/src/server/plugins/engine/types.js'
|
|
47
|
+
import {
|
|
48
|
+
DEFAULT_PAYMENT_HELP_URL,
|
|
49
|
+
formatPaymentAmount,
|
|
50
|
+
formatPaymentDate
|
|
51
|
+
} from '~/src/server/plugins/payment/helper.js'
|
|
34
52
|
import {
|
|
35
53
|
FormAction,
|
|
36
54
|
type FormRequest,
|
|
@@ -65,11 +83,25 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
65
83
|
const viewModel = new SummaryViewModel(request, this, context)
|
|
66
84
|
|
|
67
85
|
const { query } = request
|
|
68
|
-
const { payload, errors } = context
|
|
86
|
+
const { payload, errors, state } = context
|
|
87
|
+
|
|
88
|
+
const paymentField = context.relevantPages
|
|
89
|
+
.flatMap((page) => page.collection.fields)
|
|
90
|
+
.find((field): field is PaymentField => field instanceof PaymentField)
|
|
91
|
+
|
|
92
|
+
if (paymentField) {
|
|
93
|
+
const paymentState = paymentField.getPaymentStateFromState(state)
|
|
94
|
+
if (paymentState) {
|
|
95
|
+
viewModel.paymentState = paymentState
|
|
96
|
+
viewModel.paymentDetails = this.buildPaymentDetails(
|
|
97
|
+
paymentField,
|
|
98
|
+
paymentState
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
const components = this.collection.getViewModel(payload, errors, query)
|
|
70
104
|
|
|
71
|
-
// We already figure these out in the base page controller. Take them and apply them to our page-specific model.
|
|
72
|
-
// This is a stop-gap until we can add proper inheritance in place.
|
|
73
105
|
viewModel.backLink = this.getBackLink(request, context)
|
|
74
106
|
viewModel.feedbackLink = this.feedbackLink
|
|
75
107
|
viewModel.phaseTag = this.phaseTag
|
|
@@ -80,6 +112,40 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
80
112
|
return viewModel
|
|
81
113
|
}
|
|
82
114
|
|
|
115
|
+
private buildPaymentDetails(
|
|
116
|
+
paymentField: PaymentField,
|
|
117
|
+
paymentState: NonNullable<
|
|
118
|
+
ReturnType<PaymentField['getPaymentStateFromState']>
|
|
119
|
+
>
|
|
120
|
+
) {
|
|
121
|
+
const rows = [
|
|
122
|
+
{
|
|
123
|
+
key: { text: 'Payment for' },
|
|
124
|
+
value: { text: paymentState.description }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: { text: 'Total amount' },
|
|
128
|
+
value: { text: formatPaymentAmount(paymentState.amount) }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: { text: 'Reference' },
|
|
132
|
+
value: { text: paymentState.reference }
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
if (paymentState.preAuth?.createdAt) {
|
|
137
|
+
rows.push({
|
|
138
|
+
key: { text: 'Date of payment' },
|
|
139
|
+
value: { text: formatPaymentDate(paymentState.preAuth.createdAt) }
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
title: { text: 'Payment details' },
|
|
145
|
+
summaryList: { rows }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
83
149
|
/**
|
|
84
150
|
* Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,
|
|
85
151
|
*/
|
|
@@ -110,7 +176,6 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
110
176
|
context: FormContext,
|
|
111
177
|
h: FormResponseToolkit
|
|
112
178
|
) => {
|
|
113
|
-
// Check if this is a save-and-exit action
|
|
114
179
|
const { action } = request.payload
|
|
115
180
|
if (action === FormAction.SaveAndExit) {
|
|
116
181
|
return this.handleSaveAndExit(request, context, h)
|
|
@@ -133,14 +198,12 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
133
198
|
const { formsService } = this.model.services
|
|
134
199
|
const { getFormMetadata } = formsService
|
|
135
200
|
|
|
136
|
-
// Get the form metadata using the `slug` param
|
|
137
201
|
const formMetadata = await getFormMetadata(params.slug)
|
|
138
202
|
const { notificationEmail } = formMetadata
|
|
139
203
|
const { isPreview } = checkFormStatus(request.params)
|
|
140
204
|
|
|
141
205
|
checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview)
|
|
142
206
|
|
|
143
|
-
// Send submission email
|
|
144
207
|
if (notificationEmail) {
|
|
145
208
|
const viewModel = this.getSummaryViewModel(request, context)
|
|
146
209
|
|
|
@@ -155,20 +218,7 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
155
218
|
formMetadata
|
|
156
219
|
)
|
|
157
220
|
} catch (error) {
|
|
158
|
-
|
|
159
|
-
const govukError = createError(
|
|
160
|
-
error.component.name,
|
|
161
|
-
error.userMessage
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
|
|
165
|
-
|
|
166
|
-
await cacheService.resetComponentStates(request, error.getStateKeys())
|
|
167
|
-
|
|
168
|
-
return this.proceed(request, h, error.component.page?.path)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
throw error
|
|
221
|
+
return this.handleSubmissionError(error, request, h)
|
|
172
222
|
}
|
|
173
223
|
}
|
|
174
224
|
|
|
@@ -178,12 +228,103 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
178
228
|
referenceNumber: context.referenceNumber
|
|
179
229
|
} as FormConfirmationState)
|
|
180
230
|
|
|
181
|
-
// Clear all form data
|
|
182
231
|
await cacheService.clearState(request)
|
|
183
232
|
|
|
184
233
|
return this.proceed(request, h, this.getStatusPath())
|
|
185
234
|
}
|
|
186
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Handles errors during form submission
|
|
238
|
+
*/
|
|
239
|
+
private async handleSubmissionError(
|
|
240
|
+
error: unknown,
|
|
241
|
+
request: FormRequestPayload,
|
|
242
|
+
h: FormResponseToolkit
|
|
243
|
+
) {
|
|
244
|
+
if (error instanceof InvalidComponentStateError) {
|
|
245
|
+
return this.handleInvalidComponentStateError(error, request, h)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (error instanceof PaymentPreAuthError) {
|
|
249
|
+
return this.handlePaymentPreAuthError(error, request, h)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (error instanceof PaymentSubmissionError) {
|
|
253
|
+
return this.handlePaymentSubmissionError(error, request, h)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
throw error
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handles InvalidComponentStateError during submission
|
|
261
|
+
*/
|
|
262
|
+
private async handleInvalidComponentStateError(
|
|
263
|
+
error: InvalidComponentStateError,
|
|
264
|
+
request: FormRequestPayload,
|
|
265
|
+
h: FormResponseToolkit
|
|
266
|
+
) {
|
|
267
|
+
const cacheService = getCacheService(request.server)
|
|
268
|
+
|
|
269
|
+
const govukError = createError(error.component.name, error.userMessage)
|
|
270
|
+
|
|
271
|
+
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
|
|
272
|
+
|
|
273
|
+
await cacheService.resetComponentStates(request, error.getStateKeys())
|
|
274
|
+
|
|
275
|
+
return this.proceed(request, h, error.component.page?.path)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Handles PaymentPreAuthError during submission
|
|
280
|
+
*/
|
|
281
|
+
private async handlePaymentPreAuthError(
|
|
282
|
+
error: PaymentPreAuthError,
|
|
283
|
+
request: FormRequestPayload,
|
|
284
|
+
h: FormResponseToolkit
|
|
285
|
+
) {
|
|
286
|
+
const cacheService = getCacheService(request.server)
|
|
287
|
+
|
|
288
|
+
if (error.shouldResetState) {
|
|
289
|
+
await cacheService.resetComponentStates(request, error.getStateKeys())
|
|
290
|
+
|
|
291
|
+
if (error.errorType === PaymentErrorTypes.PaymentExpired) {
|
|
292
|
+
request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)
|
|
293
|
+
return this.proceed(request, h, error.component.page?.path)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const govukError = createError(error.component.name, error.userMessage)
|
|
298
|
+
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
|
|
299
|
+
|
|
300
|
+
const redirectPath = error.shouldResetState
|
|
301
|
+
? error.component.page?.path
|
|
302
|
+
: undefined
|
|
303
|
+
|
|
304
|
+
return this.proceed(request, h, redirectPath)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Handles PaymentSubmissionError during submission
|
|
309
|
+
*/
|
|
310
|
+
private handlePaymentSubmissionError(
|
|
311
|
+
error: PaymentSubmissionError,
|
|
312
|
+
request: FormRequestPayload,
|
|
313
|
+
h: FormResponseToolkit
|
|
314
|
+
) {
|
|
315
|
+
const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL
|
|
316
|
+
const helpLinkHtml = ` or you can <a href="${helpUrl}" target="_blank" rel="noopener noreferrer" class="govuk-link">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`
|
|
317
|
+
|
|
318
|
+
const govukError = createError(
|
|
319
|
+
'submission',
|
|
320
|
+
`There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
|
|
324
|
+
|
|
325
|
+
return this.proceed(request, h)
|
|
326
|
+
}
|
|
327
|
+
|
|
187
328
|
get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {
|
|
188
329
|
return {
|
|
189
330
|
ext: {
|
|
@@ -208,39 +349,66 @@ export async function submitForm(
|
|
|
208
349
|
) {
|
|
209
350
|
await finaliseComponents(request, metadata, context)
|
|
210
351
|
|
|
352
|
+
const paymentWasCaptured = hasPaymentBeenCaptured(context)
|
|
353
|
+
|
|
211
354
|
const formStatus = checkFormStatus(request.params)
|
|
212
355
|
const logTags = ['submit', 'submissionApi']
|
|
213
356
|
|
|
214
357
|
request.logger.info(logTags, 'Preparing email', formStatus)
|
|
215
358
|
|
|
216
|
-
// Get detail items
|
|
217
359
|
const items = getFormSubmissionData(
|
|
218
360
|
summaryViewModel.context,
|
|
219
361
|
summaryViewModel.details
|
|
220
362
|
)
|
|
221
363
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
364
|
+
try {
|
|
365
|
+
request.logger.info(logTags, 'Submitting data')
|
|
366
|
+
const submitResponse = await submitData(
|
|
367
|
+
model,
|
|
368
|
+
items,
|
|
369
|
+
emailAddress,
|
|
370
|
+
request.yar.id
|
|
371
|
+
)
|
|
230
372
|
|
|
231
|
-
|
|
232
|
-
|
|
373
|
+
if (submitResponse === undefined) {
|
|
374
|
+
throw Boom.badRequest('Unexpected empty response from submit api')
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
await model.services.outputService.submit(
|
|
378
|
+
context,
|
|
379
|
+
request,
|
|
380
|
+
model,
|
|
381
|
+
emailAddress,
|
|
382
|
+
items,
|
|
383
|
+
submitResponse,
|
|
384
|
+
formMetadata
|
|
385
|
+
)
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (paymentWasCaptured) {
|
|
388
|
+
throw new PaymentSubmissionError(
|
|
389
|
+
context.referenceNumber,
|
|
390
|
+
formMetadata.contact?.online?.url
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
throw err
|
|
233
394
|
}
|
|
395
|
+
}
|
|
234
396
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Checks if any payment component has been captured
|
|
399
|
+
*/
|
|
400
|
+
function hasPaymentBeenCaptured(context: FormContext): boolean {
|
|
401
|
+
for (const page of context.relevantPages) {
|
|
402
|
+
for (const field of page.collection.fields) {
|
|
403
|
+
if (field instanceof PaymentField) {
|
|
404
|
+
const paymentState = field.getPaymentStateFromState(context.state)
|
|
405
|
+
if (paymentState?.capture?.status === 'success') {
|
|
406
|
+
return true
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return false
|
|
244
412
|
}
|
|
245
413
|
|
|
246
414
|
/**
|
|
@@ -280,43 +448,50 @@ function submitData(
|
|
|
280
448
|
const payload: SubmitPayload = {
|
|
281
449
|
sessionId,
|
|
282
450
|
retrievalKey,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
main: items
|
|
286
|
-
.filter((item) => 'field' in item)
|
|
287
|
-
.map((item) => ({
|
|
288
|
-
name: item.name,
|
|
289
|
-
title: item.label,
|
|
290
|
-
value: getAnswer(item.field, item.state, { format: 'data' })
|
|
291
|
-
})),
|
|
292
|
-
|
|
293
|
-
// Repeater form answers
|
|
294
|
-
repeaters: items
|
|
295
|
-
.filter((item) => 'subItems' in item)
|
|
296
|
-
.map((item) => ({
|
|
297
|
-
name: item.name,
|
|
298
|
-
title: item.label,
|
|
299
|
-
|
|
300
|
-
// Repeater item values
|
|
301
|
-
value: item.subItems.map((detailItems) =>
|
|
302
|
-
detailItems.map((subItem) => ({
|
|
303
|
-
name: subItem.name,
|
|
304
|
-
title: subItem.label,
|
|
305
|
-
value: getAnswer(subItem.field, subItem.state, { format: 'data' })
|
|
306
|
-
}))
|
|
307
|
-
)
|
|
308
|
-
}))
|
|
451
|
+
main: buildMainRecords(items),
|
|
452
|
+
repeaters: buildRepeaterRecords(items)
|
|
309
453
|
}
|
|
310
454
|
|
|
311
455
|
return submit(payload)
|
|
312
456
|
}
|
|
313
457
|
|
|
314
458
|
export function getFormSubmissionData(context: FormContext, details: Detail[]) {
|
|
315
|
-
|
|
459
|
+
const items = context.relevantPages
|
|
316
460
|
.map(({ href }) =>
|
|
317
461
|
details.flatMap(({ items }) =>
|
|
318
462
|
items.filter(({ page }) => page.href === href)
|
|
319
463
|
)
|
|
320
464
|
)
|
|
321
465
|
.flat()
|
|
466
|
+
|
|
467
|
+
const paymentItems = getPaymentFieldItems(context)
|
|
468
|
+
|
|
469
|
+
return [...items, ...paymentItems]
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Gets DetailItems for PaymentField components
|
|
474
|
+
* PaymentField is excluded from summaryDetails for UI but needs to be in submission data
|
|
475
|
+
*/
|
|
476
|
+
function getPaymentFieldItems(context: FormContext): DetailItemField[] {
|
|
477
|
+
const items: DetailItemField[] = []
|
|
478
|
+
|
|
479
|
+
for (const page of context.relevantPages) {
|
|
480
|
+
for (const field of page.collection.fields) {
|
|
481
|
+
if (field instanceof PaymentField) {
|
|
482
|
+
items.push({
|
|
483
|
+
name: field.name,
|
|
484
|
+
page,
|
|
485
|
+
title: field.title,
|
|
486
|
+
label: field.label,
|
|
487
|
+
field,
|
|
488
|
+
state: context.state,
|
|
489
|
+
href: page.href,
|
|
490
|
+
value: field.getDisplayStringFromState(context.state)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return items
|
|
322
497
|
}
|
|
@@ -3,7 +3,10 @@ import { ComponentType } from '@defra/forms-model'
|
|
|
3
3
|
import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
4
4
|
import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
|
|
5
5
|
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
InvalidComponentStateError,
|
|
8
|
+
PaymentSubmissionError
|
|
9
|
+
} from '~/src/server/plugins/engine/pageControllers/errors.js'
|
|
7
10
|
import definition from '~/test/form/definitions/file-upload-basic.js'
|
|
8
11
|
|
|
9
12
|
describe('InvalidComponentStateError', () => {
|
|
@@ -63,4 +66,13 @@ describe('InvalidComponentStateError', () => {
|
|
|
63
66
|
expect(stateKeys).toEqual(['textField'])
|
|
64
67
|
})
|
|
65
68
|
})
|
|
69
|
+
|
|
70
|
+
describe('PaymentSubmissionError', () => {
|
|
71
|
+
it('should instantiate', () => {
|
|
72
|
+
const error = new PaymentSubmissionError('reference-number', '/help-link')
|
|
73
|
+
expect(error).toBeDefined()
|
|
74
|
+
expect(error.referenceNumber).toBe('reference-number')
|
|
75
|
+
expect(error.helpLink).toBe('/help-link')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
66
78
|
})
|
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
2
2
|
|
|
3
|
+
export enum PaymentErrorTypes {
|
|
4
|
+
PaymentExpired = 'PaymentExpired',
|
|
5
|
+
PaymentIncomplete = 'PaymentIncomplete',
|
|
6
|
+
PaymentAmountMismatch = 'PaymentAmountMismatch'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getStateKeys(component: FormComponent) {
|
|
10
|
+
const extraStateKeys = component.page?.getStateKeys(component) ?? []
|
|
11
|
+
|
|
12
|
+
return [component.name].concat(extraStateKeys)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class PaymentPreAuthError extends Error {
|
|
16
|
+
public readonly component: FormComponent
|
|
17
|
+
public readonly userMessage: string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether to reset the component state and redirect to the component's page.
|
|
21
|
+
* - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)
|
|
22
|
+
* - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)
|
|
23
|
+
*/
|
|
24
|
+
public readonly shouldResetState: boolean
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* When supplied, an "Important" notification banner will be shown based on the value.
|
|
28
|
+
*/
|
|
29
|
+
public readonly errorType: PaymentErrorTypes | undefined
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
component: FormComponent,
|
|
33
|
+
userMessage: string,
|
|
34
|
+
shouldResetState: boolean,
|
|
35
|
+
errorType?: PaymentErrorTypes
|
|
36
|
+
) {
|
|
37
|
+
super('Payment capture failed')
|
|
38
|
+
this.name = 'PaymentPreAuthError'
|
|
39
|
+
this.component = component
|
|
40
|
+
this.userMessage = userMessage
|
|
41
|
+
this.shouldResetState = shouldResetState
|
|
42
|
+
this.errorType = errorType
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getStateKeys() {
|
|
46
|
+
return getStateKeys(this.component)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when form submission fails after payment has been captured.
|
|
52
|
+
* User needs to retry or contact support for a refund.
|
|
53
|
+
*/
|
|
54
|
+
export class PaymentSubmissionError extends Error {
|
|
55
|
+
public readonly referenceNumber: string
|
|
56
|
+
public readonly helpLink?: string
|
|
57
|
+
|
|
58
|
+
constructor(referenceNumber: string, helpLink?: string) {
|
|
59
|
+
super('Form submission failed after payment capture')
|
|
60
|
+
this.name = 'PaymentSubmissionError'
|
|
61
|
+
this.referenceNumber = referenceNumber
|
|
62
|
+
this.helpLink = helpLink
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static checkPaymentAmount(
|
|
66
|
+
stateAmount: number,
|
|
67
|
+
definitionAmount: number | undefined,
|
|
68
|
+
component: FormComponent
|
|
69
|
+
) {
|
|
70
|
+
if (stateAmount / 100 !== definitionAmount) {
|
|
71
|
+
throw new PaymentPreAuthError(
|
|
72
|
+
component,
|
|
73
|
+
'The pre-authorised payment amount is somehow different from that requested. Try adding payment details again.',
|
|
74
|
+
true,
|
|
75
|
+
PaymentErrorTypes.PaymentIncomplete
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
3
81
|
/**
|
|
4
82
|
* Thrown when a component has an invalid state. This is typically only required where state needs
|
|
5
83
|
* to be checked against an external source upon submission of a form. For example: file upload
|
|
@@ -21,9 +99,6 @@ export class InvalidComponentStateError extends Error {
|
|
|
21
99
|
}
|
|
22
100
|
|
|
23
101
|
getStateKeys() {
|
|
24
|
-
|
|
25
|
-
this.component.page?.getStateKeys(this.component) ?? []
|
|
26
|
-
|
|
27
|
-
return [this.component.name].concat(extraStateKeys)
|
|
102
|
+
return getStateKeys(this.component)
|
|
28
103
|
}
|
|
29
104
|
}
|