@@ -10,6 +10,7 @@ import joi, { type ObjectSchema } from 'joi'
1010import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
1111import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
1212import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
13+ import { InvalidComponentStateError } from '~/src/server/plugins/engine/pageControllers/errors.js'
1314import {
1415 type AnyFormRequest ,
1516 type FormContext ,
@@ -47,6 +48,7 @@ export class PaymentField extends FormComponent {
4748 amount : joi . number ( ) . required ( ) ,
4849 description : joi . string ( ) . required ( ) ,
4950 uuid : joi . string ( ) . uuid ( ) . required ( ) ,
51+ isLive : joi . boolean ( ) . required ( ) ,
5052 preAuth : joi
5153 . object ( {
5254 status : joi
@@ -155,7 +157,8 @@ export class PaymentField extends FormComponent {
155157 h : FormResponseToolkit ,
156158 args : PaymentDispatcherArgs
157159 ) : Promise < unknown > {
158- const paymentService = new PaymentService ( )
160+ const { isLive } = args
161+ const paymentService = new PaymentService ( { isLive } )
159162
160163 // 1. Generate UUID token
161164 const uuid = randomUUID ( )
@@ -173,18 +176,19 @@ export class PaymentField extends FormComponent {
173176
174177 // 2. Build the return URL for GOV.UK Pay
175178 const { baseUrl } = getPluginOptions ( request . server )
176- const returnUrl = `${ baseUrl } /payment-callback?uuid=${ uuid } `
179+ const payCallbackUrl = `${ baseUrl } /payment-callback?uuid=${ uuid } `
177180
178- // Build the summary URL to redirect to after payment
181+ // Build URLs for redirect after payment
179182 const summaryUrl = `${ baseUrl } /${ model . basePath } /summary`
183+ const paymentPageUrl = args . sourceUrl
180184
181185 // 3. Call paymentService.createPayment()
182186 // GOV.UK Pay expects amount in pence, so multiply pounds by 100
183187 const amountInPence = Math . round ( amount * 100 )
184188 const payment = await paymentService . createPayment (
185189 amountInPence ,
186190 description ,
187- returnUrl ,
191+ payCallbackUrl ,
188192 reference ,
189193 { formId, slug }
190194 )
@@ -197,7 +201,9 @@ export class PaymentField extends FormComponent {
197201 description,
198202 paymentId : payment . paymentId ,
199203 componentName,
200- sourceUrl : summaryUrl
204+ returnUrl : summaryUrl ,
205+ failureUrl : paymentPageUrl ,
206+ isLive
201207 }
202208
203209 request . yar . set ( `payment-${ uuid } ` , sessionData )
@@ -208,21 +214,86 @@ export class PaymentField extends FormComponent {
208214
209215 /**
210216 * Called on form submission to capture the payment
211- * STUB - Jez to implement
217+ * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment
212218 */
213- onSubmit (
214- _request : FormRequestPayload ,
219+ async onSubmit (
220+ request : FormRequestPayload ,
215221 _metadata : FormMetadata ,
216- _context : FormContext
222+ context : FormContext
217223 ) : Promise < void > {
218- // TODO: Implement
219- // 1. Get payment state from context
220- // 2. If already captured, skip
221- // 3. Call paymentService.getPaymentStatus() to validate pre-auth
222- // 4. Call paymentService.capturePayment()
223- // 5. Update payment state with capture status
224- // 6. If capture fails, throw InvalidComponentStateError
225- return Promise . resolve ( )
224+ const paymentState = this . getPaymentStateFromState ( context . state )
225+
226+ if ( ! paymentState ) {
227+ // No payment state - redirect to payment page to complete payment
228+ throw new InvalidComponentStateError (
229+ this ,
230+ 'Complete the payment to continue' ,
231+ { shouldResetState : true }
232+ )
233+ }
234+
235+ // Skip if already captured
236+ if ( paymentState . capture ?. status === 'success' ) {
237+ return
238+ }
239+
240+ const { paymentId, isLive } = paymentState
241+ const paymentService = new PaymentService ( { isLive } )
242+
243+ // Verify payment is still in capturable state
244+ const status = await paymentService . getPaymentStatus ( paymentId )
245+
246+ // If already captured (success state), mark as captured and continue
247+ if ( status . state . status === 'success' ) {
248+ await this . markPaymentCaptured ( request , paymentState )
249+ return
250+ }
251+
252+ if ( status . state . status !== 'capturable' ) {
253+ throw new InvalidComponentStateError (
254+ this ,
255+ 'Your payment authorisation has expired. Please add your payment details again.' ,
256+ { shouldResetState : true }
257+ )
258+ }
259+
260+ // Capture the payment
261+ const captured = await paymentService . capturePayment ( paymentId )
262+
263+ if ( ! captured ) {
264+ throw new InvalidComponentStateError (
265+ this ,
266+ 'There was a problem and your form was not submitted. Try submitting the form again.' ,
267+ { shouldResetState : false }
268+ )
269+ }
270+
271+ await this . markPaymentCaptured ( request , paymentState )
272+ }
273+
274+ /**
275+ * Updates payment state to mark capture as successful
276+ * This ensures we don't try to re-capture on submission retry
277+ */
278+ private async markPaymentCaptured (
279+ request : FormRequestPayload ,
280+ paymentState : PaymentState
281+ ) : Promise < void > {
282+ const updatedState : PaymentState = {
283+ ...paymentState ,
284+ capture : {
285+ status : 'success' ,
286+ createdAt : new Date ( ) . toISOString ( )
287+ }
288+ }
289+
290+ // Update the state in the page controller
291+ if ( this . page ) {
292+ const currentState = await this . page . getState ( request )
293+ await this . page . mergeState ( request , currentState , {
294+ [ this . name ] : updatedState
295+ } )
296+ }
226297 }
227298}
228299
@@ -237,7 +308,7 @@ export interface PaymentDispatcherArgs {
237308 }
238309 component : PaymentField
239310 sourceUrl : string
240- paymentService : PaymentService
311+ isLive : boolean
241312}
242313
243314/**
@@ -250,5 +321,7 @@ export interface PaymentSessionData {
250321 description : string
251322 paymentId : string
252323 componentName : string
253- sourceUrl : string
324+ returnUrl : string
325+ failureUrl : string
326+ isLive : boolean
254327}
0 commit comments