Skip to content

Commit 5debb87

Browse files
committed
refactor(sonar): reduce complexity
1 parent 960c118 commit 5debb87

2 files changed

Lines changed: 148 additions & 97 deletions

File tree

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

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -230,61 +230,89 @@ export class SummaryPageController extends QuestionPageController {
230230
formMetadata
231231
)
232232
} catch (error) {
233-
if (error instanceof InvalidComponentStateError) {
234-
if (error.shouldResetState) {
235-
await cacheService.resetComponentStates(
236-
request,
237-
error.getStateKeys()
238-
)
239-
240-
if (error.isPaymentExpired) {
241-
request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)
242-
return this.proceed(request, h, error.component.page?.path)
243-
}
244-
}
233+
return this.handleSubmissionError(error, request, h)
234+
}
235+
}
245236

246-
const govukError = createError(
247-
error.component.name,
248-
error.userMessage
249-
)
237+
await cacheService.setConfirmationState(request, {
238+
confirmed: true,
239+
formId: context.state.formId
240+
} as FormConfirmationState)
250241

251-
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
242+
// Clear all form data
243+
await cacheService.clearState(request)
252244

253-
if (error.shouldResetState) {
254-
return this.proceed(request, h, error.component.page?.path)
255-
}
245+
return this.proceed(request, h, this.getStatusPath())
246+
}
256247

257-
return this.proceed(request, h)
258-
}
248+
/**
249+
* Handles errors during form submission
250+
*/
251+
private async handleSubmissionError(
252+
error: unknown,
253+
request: FormRequestPayload,
254+
h: FormResponseToolkit
255+
) {
256+
if (error instanceof InvalidComponentStateError) {
257+
return this.handleInvalidComponentStateError(error, request, h)
258+
}
259259

260-
if (error instanceof PostPaymentSubmissionError) {
261-
const helpLink = error.helpLink
262-
? ` or you can <a href="${error.helpLink}" target="_blank" rel="noopener noreferrer" class="govuk-link">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`
263-
: ''
260+
if (error instanceof PostPaymentSubmissionError) {
261+
return this.handlePostPaymentSubmissionError(error, request, h)
262+
}
264263

265-
const govukError = createError(
266-
'submission',
267-
`There was a problem and your form was not submitted. Try submitting the form again${helpLink}.`
268-
)
264+
throw error
265+
}
269266

270-
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
267+
/**
268+
* Handles InvalidComponentStateError during submission
269+
*/
270+
private async handleInvalidComponentStateError(
271+
error: InvalidComponentStateError,
272+
request: FormRequestPayload,
273+
h: FormResponseToolkit
274+
) {
275+
const cacheService = getCacheService(request.server)
271276

272-
return this.proceed(request, h)
273-
}
277+
if (error.shouldResetState) {
278+
await cacheService.resetComponentStates(request, error.getStateKeys())
274279

275-
throw error
280+
if (error.isPaymentExpired) {
281+
request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)
282+
return this.proceed(request, h, error.component.page?.path)
276283
}
277284
}
278285

279-
await cacheService.setConfirmationState(request, {
280-
confirmed: true,
281-
formId: context.state.formId
282-
} as FormConfirmationState)
286+
const govukError = createError(error.component.name, error.userMessage)
287+
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
283288

284-
// Clear all form data
285-
await cacheService.clearState(request)
289+
const redirectPath = error.shouldResetState
290+
? error.component.page?.path
291+
: undefined
286292

287-
return this.proceed(request, h, this.getStatusPath())
293+
return this.proceed(request, h, redirectPath)
294+
}
295+
296+
/**
297+
* Handles PostPaymentSubmissionError during submission
298+
*/
299+
private handlePostPaymentSubmissionError(
300+
error: PostPaymentSubmissionError,
301+
request: FormRequestPayload,
302+
h: FormResponseToolkit
303+
) {
304+
const helpLink = error.helpLink
305+
? ` or you can <a href="${error.helpLink}" target="_blank" rel="noopener noreferrer" class="govuk-link">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`
306+
: ''
307+
308+
const govukError = createError(
309+
'submission',
310+
`There was a problem and your form was not submitted. Try submitting the form again${helpLink}.`
311+
)
312+
313+
request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)
314+
315+
return this.proceed(request, h)
288316
}
289317

290318
get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {

src/server/plugins/engine/routes/payment.js

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,61 @@ export function getRoutes() {
4848
return [getReturnRoute()]
4949
}
5050

51+
/**
52+
* Validates session data and retrieves payment status
53+
* @param {Request} request - the request
54+
* @param {string} uuid - the payment UUID
55+
* @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}
56+
*/
57+
async function getPaymentContext(request, uuid) {
58+
const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`
59+
const session = /** @type {PaymentSessionData | null} */ (
60+
request.yar.get(sessionKey)
61+
)
62+
63+
if (!session) {
64+
throw Boom.badRequest(`No payment session found for uuid=${uuid}`)
65+
}
66+
67+
const { paymentId, isLivePayment, formId } = session
68+
69+
if (!paymentId) {
70+
throw Boom.badRequest('No paymentId in session')
71+
}
72+
73+
const apiKey = getPaymentApiKey(isLivePayment, formId)
74+
const paymentService = new PaymentService(apiKey)
75+
const paymentStatus = await paymentService.getPaymentStatus(paymentId)
76+
77+
return { session, sessionKey, paymentStatus }
78+
}
79+
80+
/**
81+
* Handles successful payment states (capturable/success)
82+
* @param {Request} request - the request
83+
* @param {ResponseToolkit} h - the response toolkit
84+
* @param {PaymentSessionData} session - the session data
85+
* @param {string} sessionKey - the session key
86+
* @param {string} paymentId - the payment id
87+
*/
88+
function handlePaymentSuccess(request, h, session, sessionKey, paymentId) {
89+
flashComponentState(request, session, paymentId)
90+
request.yar.clear(sessionKey)
91+
return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)
92+
}
93+
94+
/**
95+
* Handles failed/cancelled/error payment states
96+
* @param {Request} request - the request
97+
* @param {ResponseToolkit} h - the response toolkit
98+
* @param {PaymentSessionData} session - the session data
99+
* @param {string} sessionKey - the session key
100+
*/
101+
function handlePaymentFailure(request, h, session, sessionKey) {
102+
request.yar.clear(sessionKey)
103+
return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)
104+
}
105+
51106
/**
52107
* Route handler for payment return URL
53108
* This is called when GOV.UK Pay redirects the user back after payment
@@ -59,83 +114,50 @@ function getReturnRoute() {
59114
path: PAYMENT_RETURN_PATH,
60115
async handler(request, h) {
61116
const { uuid } = /** @type {{ uuid: string }} */ (request.query)
62-
63-
// 1. Get session data using the UUID as the key
64-
const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`
65-
const session = /** @type {PaymentSessionData | null} */ (
66-
request.yar.get(sessionKey)
117+
const { session, sessionKey, paymentStatus } = await getPaymentContext(
118+
request,
119+
uuid
67120
)
68121

69-
if (!session) {
70-
throw Boom.badRequest(`No payment session found for uuid=${uuid}`)
71-
}
72-
73-
// 2. Get payment status from GOV.UK Pay
74-
const { paymentId, isLivePayment, formId } = session
75-
76-
if (!paymentId) {
77-
throw Boom.badRequest('No paymentId in session')
78-
}
79-
80-
const apiKey = getPaymentApiKey(isLivePayment, formId)
81-
const paymentService = new PaymentService(apiKey)
82-
const paymentStatus = await paymentService.getPaymentStatus(paymentId)
83-
84-
// 3. Handle different payment states based on GOV.UK Pay status lifecycle
122+
// Handle different payment states based on GOV.UK Pay status lifecycle
85123
// @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
86124
const { status } = paymentStatus.state
87125

88126
switch (status) {
127+
// Pre-auth successful or already captured
89128
case 'capturable':
90-
// Pre-auth successful - flash the state and redirect to summary
91-
flashComponentState(request, session, paymentId)
92-
request.yar.clear(sessionKey)
93-
return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)
94-
95129
case 'success':
96-
// Payment already captured (shouldn't happen with delayed_capture: true)
97-
flashComponentState(request, session, paymentId)
98-
request.yar.clear(sessionKey)
99-
return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)
130+
return handlePaymentSuccess(
131+
request,
132+
h,
133+
session,
134+
sessionKey,
135+
session.paymentId
136+
)
100137

138+
// Payment failed, cancelled, or errored - redirect to retry
101139
case 'cancelled':
102-
// User cancelled payment (P0030) - redirect to payment page to retry
103-
request.yar.clear(sessionKey)
104-
return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)
105-
106140
case 'failed':
107-
// Payment failed (P0010 rejected, P0020 expired, P0040 service cancelled, P0050 provider error)
108-
// Redirect to payment page to retry
109-
request.yar.clear(sessionKey)
110-
return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)
111-
112141
case 'error':
113-
// Technical error on GOV.UK Pay side - no funds taken
114-
// Redirect to payment page to retry
115-
request.yar.clear(sessionKey)
116-
return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)
142+
return handlePaymentFailure(request, h, session, sessionKey)
117143

144+
// User came back too early - redirect back to GOV.UK Pay
118145
case 'created':
119146
case 'started':
120147
case 'submitted': {
121-
// User came back too early or payment still processing
122-
// Redirect back to GOV.UK Pay to continue
123148
const nextUrl = paymentStatus._links.next_url?.href
124149

125-
if (nextUrl) {
126-
return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER)
150+
if (!nextUrl) {
151+
throw Boom.badRequest(
152+
`Payment in state '${status}' but no next_url available`
153+
)
127154
}
128155

129-
throw Boom.badRequest(
130-
`Payment in state '${status}' but no next_url available`
131-
)
156+
return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER)
132157
}
133158

134-
default: {
135-
// this should never be reached but Sonar will complain
136-
const unknownStatus = /** @type {string} */ (status)
137-
throw Boom.internal(`Unknown payment status: ${unknownStatus}`)
138-
}
159+
default:
160+
throw Boom.internal(`Unknown payment status: ${String(status)}`)
139161
}
140162
},
141163
options: {
@@ -166,7 +188,8 @@ function getReturnRoute() {
166188
*/
167189

168190
/**
169-
* @import { Request, ServerRoute } from '@hapi/hapi'
191+
* @import { Request, ResponseToolkit, ServerRoute } from '@hapi/hapi'
170192
* @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
193+
* @import { GetPaymentResponse } from '~/src/server/plugins/payment/types.js'
171194
* @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'
172195
*/

0 commit comments

Comments
 (0)