@@ -5,9 +5,11 @@ import {
55 type PaymentFieldComponent
66} from '@defra/forms-model'
77import { StatusCodes } from 'http-status-codes'
8+ import joi , { type ObjectSchema } from 'joi'
89
910import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
1011import { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'
12+ import { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'
1113import {
1214 type AnyFormRequest ,
1315 type FormContext ,
@@ -17,13 +19,17 @@ import {
1719import {
1820 type ErrorMessageTemplateList ,
1921 type FormPayload ,
22+ type FormState ,
23+ type FormStateValue ,
2024 type FormSubmissionError ,
2125 type FormSubmissionState
2226} from '~/src/server/plugins/engine/types.js'
2327import { PaymentService } from '~/src/server/plugins/payment/service.js'
2428
2529export class PaymentField extends FormComponent {
2630 declare options : PaymentFieldComponent [ 'options' ]
31+ declare formSchema : ObjectSchema
32+ declare stateSchema : ObjectSchema
2733
2834 constructor (
2935 def : PaymentFieldComponent ,
@@ -32,6 +38,30 @@ export class PaymentField extends FormComponent {
3238 super ( def , props )
3339
3440 this . options = def . options
41+
42+ // Payment state is validated as an object with the required fields
43+ const paymentStateSchema = joi
44+ . object ( {
45+ paymentId : joi . string ( ) . required ( ) ,
46+ reference : joi . string ( ) . required ( ) ,
47+ amount : joi . number ( ) . required ( ) ,
48+ description : joi . string ( ) . required ( ) ,
49+ uuid : joi . string ( ) . uuid ( ) . required ( ) ,
50+ preAuth : joi
51+ . object ( {
52+ status : joi
53+ . string ( )
54+ . valid ( 'success' , 'failed' , 'started' )
55+ . required ( ) ,
56+ createdAt : joi . string ( ) . isoDate ( ) . required ( )
57+ } )
58+ . required ( )
59+ } )
60+ . unknown ( true )
61+ . label ( this . label )
62+
63+ this . formSchema = paymentStateSchema
64+ this . stateSchema = paymentStateSchema . default ( null ) . allow ( null )
3565 }
3666
3767 /**
@@ -40,7 +70,7 @@ export class PaymentField extends FormComponent {
4070 getPaymentStateFromState (
4171 state : FormSubmissionState
4272 ) : PaymentState | undefined {
43- const value = state [ this . name ] as unknown
73+ const value = state [ this . name ]
4474 return this . isPaymentState ( value ) ? value : undefined
4575 }
4676
@@ -88,6 +118,13 @@ export class PaymentField extends FormComponent {
88118 )
89119 }
90120
121+ /**
122+ * Override base isState to validate PaymentState
123+ */
124+ isState ( value ?: FormStateValue | FormState ) : value is FormState {
125+ return this . isPaymentState ( value )
126+ }
127+
91128 /**
92129 * For error preview page that shows all possible errors on a component
93130 */
@@ -112,7 +149,6 @@ export class PaymentField extends FormComponent {
112149
113150 /**
114151 * Dispatcher for external redirect to GOV.UK Pay
115- * STUB - Jez to implement
116152 */
117153 static async dispatcher (
118154 request : FormRequestPayload ,
@@ -121,38 +157,52 @@ export class PaymentField extends FormComponent {
121157 ) : Promise < unknown > {
122158 const paymentService = new PaymentService ( )
123159
124- // 1. Generate UUID token and store in session
160+ // 1. Generate UUID token
125161 const uuid = randomUUID ( )
126162
127- const { options } = args . component
163+ const { options, name : componentName } = args . component
128164 const { model } = args . controller
129165
130166 const state = await args . controller . getState ( request )
131-
132- const data = {
133- uuid,
134- reference : state . $$__referenceNumber ,
135- description : options . description ,
136- amount : options . amount
137- } as PaymentState
138-
139- request . yar . set ( `${ request . url . pathname } -payment` , data )
167+ const reference = state . $$__referenceNumber as string
168+ const amount = options . amount ?? 0
169+ const description = options . description ?? ''
140170
141171 const formId = model . formId
142172 const slug = `/${ model . basePath } `
143173
144- // 2. Call paymentService.createPayment()
174+ // 2. Build the return URL for GOV.UK Pay
175+ const { baseUrl } = getPluginOptions ( request . server )
176+ const returnUrl = `${ baseUrl } /payment-callback?uuid=${ uuid } `
177+
178+ // Build the summary URL to redirect to after payment
179+ const summaryUrl = `${ baseUrl } /${ model . basePath } /summary`
180+
181+ // 3. Call paymentService.createPayment()
145182 // GOV.UK Pay expects amount in pence, so multiply pounds by 100
146- const amountInPence = Math . round ( data . amount * 100 )
183+ const amountInPence = Math . round ( amount * 100 )
147184 const payment = await paymentService . createPayment (
148185 amountInPence ,
149- data . description ,
150- uuid ,
151- data . reference ,
186+ description ,
187+ returnUrl ,
188+ reference ,
152189 { formId, slug }
153190 )
154191
155- // 3. Redirect to GOV.UK Pay paymentUrl
192+ // 4. Store session data for the return route to use
193+ const sessionData : PaymentSessionData = {
194+ uuid,
195+ reference,
196+ amount,
197+ description,
198+ paymentId : payment . paymentId ,
199+ componentName,
200+ sourceUrl : summaryUrl
201+ }
202+
203+ request . yar . set ( `payment-${ uuid } ` , sessionData )
204+
205+ // 5. Redirect to GOV.UK Pay paymentUrl
156206 return h . redirect ( payment . paymentUrl ) . code ( StatusCodes . SEE_OTHER )
157207 }
158208
@@ -189,3 +239,16 @@ export interface PaymentDispatcherArgs {
189239 sourceUrl : string
190240 paymentService : PaymentService
191241}
242+
243+ /**
244+ * Session data stored when dispatching to GOV.UK Pay
245+ */
246+ export interface PaymentSessionData {
247+ uuid : string
248+ reference : string
249+ amount : number
250+ description : string
251+ paymentId : string
252+ componentName : string
253+ sourceUrl : string
254+ }
0 commit comments