Skip to content

Commit 41ec700

Browse files
committed
Stash
1 parent 3cc88fa commit 41ec700

3 files changed

Lines changed: 82 additions & 35 deletions

File tree

src/server/plugins/engine/components/PaymentField.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import joi, { type ObjectSchema } from 'joi'
1010
import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
1111
import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
1212
import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
13-
import { InvalidComponentStateError } from '~/src/server/plugins/engine/pageControllers/errors.js'
13+
import {
14+
PaymentErrorTypes,
15+
PrePaymentError
16+
} from '~/src/server/plugins/engine/pageControllers/errors.js'
1417
import {
1518
type AnyFormRequest,
1619
type FormContext,
@@ -248,10 +251,11 @@ export class PaymentField extends FormComponent {
248251
const paymentState = this.getPaymentStateFromState(context.state)
249252

250253
if (!paymentState) {
251-
throw new InvalidComponentStateError(
254+
throw new PrePaymentError(
252255
this,
253256
'Complete the payment to continue',
254-
{ shouldResetState: true }
257+
true,
258+
PaymentErrorTypes.PaymentIncomplete
255259
)
256260
}
257261

@@ -273,20 +277,21 @@ export class PaymentField extends FormComponent {
273277
}
274278

275279
if (status.state.status !== 'capturable') {
276-
throw new InvalidComponentStateError(
280+
throw new PrePaymentError(
277281
this,
278282
'Your payment authorisation has expired. Please add your payment details again.',
279-
{ shouldResetState: true, isPaymentExpired: true }
283+
true,
284+
PaymentErrorTypes.PaymentExpired
280285
)
281286
}
282287

283288
const captured = await paymentService.capturePayment(paymentId)
284289

285290
if (!captured) {
286-
throw new InvalidComponentStateError(
291+
throw new PrePaymentError(
287292
this,
288293
'There was a problem and your form was not submitted. Try submitting the form again.',
289-
{ shouldResetState: false }
294+
false
290295
)
291296
}
292297

src/server/plugins/engine/pageControllers/SummaryPageController.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import {
3131
import { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
3232
import {
3333
InvalidComponentStateError,
34-
PostPaymentSubmissionError
34+
PaymentErrorTypes,
35+
PostPaymentSubmissionError,
36+
PrePaymentError
3537
} from '~/src/server/plugins/engine/pageControllers/errors.js'
3638
import {
3739
buildMainRecords,
@@ -243,6 +245,10 @@ export class SummaryPageController extends QuestionPageController {
243245
return this.handleInvalidComponentStateError(error, request, h)
244246
}
245247

248+
if (error instanceof PrePaymentError) {
249+
return this.handlePrePaymentError(error, request, h)
250+
}
251+
246252
if (error instanceof PostPaymentSubmissionError) {
247253
return this.handlePostPaymentSubmissionError(error, request, h)
248254
}
@@ -260,10 +266,29 @@ export class SummaryPageController extends QuestionPageController {
260266
) {
261267
const cacheService = getCacheService(request.server)
262268

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 PrePaymentError during submission
280+
*/
281+
private async handlePrePaymentError(
282+
error: PrePaymentError,
283+
request: FormRequestPayload,
284+
h: FormResponseToolkit
285+
) {
286+
const cacheService = getCacheService(request.server)
287+
263288
if (error.shouldResetState) {
264289
await cacheService.resetComponentStates(request, error.getStateKeys())
265290

266-
if (error.isPaymentExpired) {
291+
if (error.errorType === PaymentErrorTypes.PaymentExpired) {
267292
request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)
268293
return this.proceed(request, h, error.component.page?.path)
269294
}

src/server/plugins/engine/pageControllers/errors.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,47 @@
11
import { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
22

3+
export enum PaymentErrorTypes {
4+
PaymentExpired = 'PaymentExpired',
5+
PaymentIncomplete = 'PaymentIncomplete'
6+
}
7+
export class PrePaymentError extends Error {
8+
public readonly component: FormComponent
9+
public readonly userMessage: string
10+
11+
/**
12+
* Whether to reset the component state and redirect to the component's page.
13+
* - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)
14+
* - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)
15+
*/
16+
public readonly shouldResetState: boolean
17+
18+
/**
19+
* When supplied, an "Important" notification banner will be shown based on the value.
20+
*/
21+
public readonly errorType: PaymentErrorTypes | undefined
22+
23+
constructor(
24+
component: FormComponent,
25+
userMessage: string,
26+
shouldResetState: boolean,
27+
errorType?: PaymentErrorTypes
28+
) {
29+
super('Payment capture failed')
30+
this.name = 'PrePaymentError'
31+
this.component = component
32+
this.userMessage = userMessage
33+
this.shouldResetState = shouldResetState
34+
this.errorType = errorType
35+
}
36+
37+
getStateKeys() {
38+
const extraStateKeys =
39+
this.component.page?.getStateKeys(this.component) ?? []
40+
41+
return [this.component.name].concat(extraStateKeys)
42+
}
43+
}
44+
345
/**
446
* Thrown when form submission fails after payment has been captured.
547
* User needs to retry or contact support for a refund.
@@ -16,23 +58,6 @@ export class PostPaymentSubmissionError extends Error {
1658
}
1759
}
1860

19-
export interface InvalidComponentStateErrorOptions {
20-
/**
21-
* Whether to reset the component state and redirect to the component's page.
22-
* - `true`: Reset state and redirect (e.g., payment expired - user must re-enter)
23-
* - `false`: Keep state and stay on current page with error (e.g., capture failed - user can retry)
24-
* @default true
25-
*/
26-
shouldResetState?: boolean
27-
28-
/**
29-
* Whether this error is due to payment expiry.
30-
* When true, an "Important" notification banner will be shown on the payment page.
31-
* @default false
32-
*/
33-
isPaymentExpired?: boolean
34-
}
35-
3661
/**
3762
* Thrown when a component has an invalid state. This is typically only required where state needs
3863
* to be checked against an external source upon submission of a form. For example: file upload
@@ -44,21 +69,13 @@ export interface InvalidComponentStateErrorOptions {
4469
export class InvalidComponentStateError extends Error {
4570
public readonly component: FormComponent
4671
public readonly userMessage: string
47-
public readonly shouldResetState: boolean
48-
public readonly isPaymentExpired: boolean
4972

50-
constructor(
51-
component: FormComponent,
52-
userMessage: string,
53-
options: InvalidComponentStateErrorOptions = {}
54-
) {
73+
constructor(component: FormComponent, userMessage: string) {
5574
const message = `Invalid component state for: ${component.name}`
5675
super(message)
5776
this.name = 'InvalidComponentStateError'
5877
this.component = component
5978
this.userMessage = userMessage
60-
this.shouldResetState = options.shouldResetState ?? true
61-
this.isPaymentExpired = options.isPaymentExpired ?? false
6279
}
6380

6481
getStateKeys() {

0 commit comments

Comments
 (0)