@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
|
@@ -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
|
-
|
|
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
|
+
})
|