@defra/forms-engine-plugin 4.8.0 → 4.9.0

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.
Files changed (43) hide show
  1. package/.server/server/forms/payment-v2-test.yaml +341 -0
  2. package/.server/server/plugins/engine/components/PaymentField.d.ts +7 -0
  3. package/.server/server/plugins/engine/components/PaymentField.js +58 -6
  4. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
  5. package/.server/server/plugins/engine/models/SummaryViewModel.d.ts +2 -0
  6. package/.server/server/plugins/engine/models/SummaryViewModel.js +2 -0
  7. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  8. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +24 -2
  9. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  10. package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +10 -0
  11. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +57 -13
  12. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/errors.d.ts +1 -1
  14. package/.server/server/plugins/engine/pageControllers/errors.js +2 -2
  15. package/.server/server/plugins/engine/pageControllers/errors.js.map +1 -1
  16. package/.server/server/plugins/engine/routes/index.js +5 -2
  17. package/.server/server/plugins/engine/routes/index.js.map +1 -1
  18. package/.server/server/plugins/engine/routes/payment.js +6 -1
  19. package/.server/server/plugins/engine/routes/payment.js.map +1 -1
  20. package/.server/server/plugins/engine/routes/payment.test.js +3 -3
  21. package/.server/server/plugins/engine/routes/payment.test.js.map +1 -1
  22. package/.server/server/plugins/engine/services/localFormsService.js +6 -0
  23. package/.server/server/plugins/engine/services/localFormsService.js.map +1 -1
  24. package/.server/server/plugins/engine/views/summary.html +2 -1
  25. package/.server/server/plugins/payment/service.d.ts +2 -1
  26. package/.server/server/plugins/payment/service.js +11 -3
  27. package/.server/server/plugins/payment/service.js.map +1 -1
  28. package/.server/server/plugins/payment/types.d.ts +4 -0
  29. package/.server/server/plugins/payment/types.js +1 -0
  30. package/.server/server/plugins/payment/types.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/server/forms/payment-v2-test.yaml +341 -0
  33. package/src/server/plugins/engine/components/PaymentField.ts +70 -6
  34. package/src/server/plugins/engine/models/SummaryViewModel.ts +2 -0
  35. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +32 -1
  36. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +99 -17
  37. package/src/server/plugins/engine/pageControllers/errors.ts +2 -2
  38. package/src/server/plugins/engine/routes/index.ts +9 -2
  39. package/src/server/plugins/engine/routes/payment.js +7 -1
  40. package/src/server/plugins/engine/services/localFormsService.js +7 -0
  41. package/src/server/plugins/engine/views/summary.html +2 -1
  42. package/src/server/plugins/payment/service.js +13 -3
  43. package/src/server/plugins/payment/types.js +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","names":["StatusCodes","createLogger","buildPaymentInfo","convertPenceToPounds","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","isLivePayment","metadata","response","postToPayProvider","return_url","delayed_capture","info","payment_id","paymentId","paymentUrl","_links","next_url","href","err","error","output","payload","message","undefined","getPaymentStatus","getByType","headers","json","errorMessage","Error","JSON","stringify","state","status","code","email","capturePayment","statusCode","res","OK","NO_CONTENT","event","category","action","outcome","reason","postJsonByType","includes"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n buildPaymentInfo,\n convertPenceToPounds\n} from '~/src/server/plugins/engine/routes/payment-helper.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {boolean} isLivePayment\n * @param {{ formId: string, slug: string } | undefined } metadata\n */\n async createPayment(\n amount,\n description,\n returnUrl,\n reference,\n isLivePayment,\n metadata\n ) {\n try {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n buildPaymentInfo(\n 'create-payment',\n 'success',\n `amount=${convertPenceToPounds(amount)}`,\n isLivePayment,\n response.payment_id\n ),\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n } catch (err) {\n const error =\n /** @type {{ output?: { payload?: any }, message?: any }} */ (err)\n if (isLivePayment) {\n logger.error(\n error.output?.payload ?? error.message,\n `[payment] Failed to create payment session for reference ${reference}`\n )\n }\n }\n return undefined\n }\n\n /**\n * @param {string} paymentId\n * @param {boolean} isLivePayment\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId, isLivePayment) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n buildPaymentInfo(\n 'get-payment-status',\n state.status === 'capturable' || state.status === 'success'\n ? 'success'\n : 'failure',\n `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,\n isLivePayment,\n paymentId\n ),\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'capture-payment',\n outcome: 'success',\n reason: `amount=${convertPenceToPounds(amount)}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n if (!error.message.includes('401 Unauthorized')) {\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n }\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"service.js","names":["StatusCodes","createLogger","buildPaymentInfo","convertPenceToPounds","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","isLivePayment","metadata","email","payload","return_url","delayed_capture","response","postToPayProvider","info","payment_id","paymentId","paymentUrl","_links","next_url","href","err","error","output","message","undefined","getPaymentStatus","getByType","headers","json","errorMessage","Error","JSON","stringify","state","status","code","capturePayment","statusCode","res","OK","NO_CONTENT","event","category","action","outcome","reason","postJsonByType","includes"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport {\n buildPaymentInfo,\n convertPenceToPounds\n} from '~/src/server/plugins/engine/routes/payment-helper.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {boolean} isLivePayment\n * @param {{ formId: string, slug: string } | undefined } metadata\n * @param {string} [email] - optional email to prepopulate on GOV.UK Pay\n */\n async createPayment(\n amount,\n description,\n returnUrl,\n reference,\n isLivePayment,\n metadata,\n email\n ) {\n try {\n /** @type {CreatePaymentRequest} */\n const payload = {\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n }\n\n // Prepopulate email on GOV.UK Pay if provided\n if (email) {\n payload.email = email\n }\n\n const response = await this.postToPayProvider(payload)\n\n logger.info(\n buildPaymentInfo(\n 'create-payment',\n 'success',\n `amount=${convertPenceToPounds(amount)}`,\n isLivePayment,\n response.payment_id\n ),\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n } catch (err) {\n const error =\n /** @type {{ output?: { payload?: any }, message?: any }} */ (err)\n if (isLivePayment) {\n logger.error(\n error.output?.payload ?? error.message,\n `[payment] Failed to create payment session for reference ${reference}`\n )\n }\n }\n return undefined\n }\n\n /**\n * @param {string} paymentId\n * @param {boolean} isLivePayment\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId, isLivePayment) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n buildPaymentInfo(\n 'get-payment-status',\n state.status === 'capturable' || state.status === 'success'\n ? 'success'\n : 'failure',\n `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,\n isLivePayment,\n paymentId\n ),\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'capture-payment',\n outcome: 'success',\n reason: `amount=${convertPenceToPounds(amount)}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n if (!error.message.includes('401 Unauthorized')) {\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n }\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
@@ -54,6 +54,10 @@ export type CreatePaymentRequest = {
54
54
  formId: string;
55
55
  slug: string;
56
56
  } | undefined;
57
+ /**
58
+ * - Email to prepopulate on GOV.UK Pay (max 254 chars)
59
+ */
60
+ email?: string | undefined;
57
61
  };
58
62
  export type CreatePaymentResponse = {
59
63
  /**
@@ -20,6 +20,7 @@
20
20
  * @property {string} return_url - URL to redirect the user to after payment
21
21
  * @property {boolean} [delayed_capture] - Whether to delay capturing the payment
22
22
  * @property {{ formId: string, slug: string }} [metadata] - Additional metadata for the payment
23
+ * @property {string} [email] - Email to prepopulate on GOV.UK Pay (max 254 chars)
23
24
  */
24
25
 
25
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../../src/server/plugins/payment/types.js"],"sourcesContent":["/**\n * @typedef {object} PaymentResponseState\n * @property {'created' | 'started' | 'submitted' | 'capturable' | 'success' | 'failed' | 'cancelled' | 'error'} status - Current status of the payment\n * @property {boolean} finished - Whether the payment process has completed\n * @property {string} [message] - Human-readable message about the payment state\n * @property {string} [code] - Error or status code for the payment state\n */\n\n/**\n * @typedef {object} PaymentLink\n * @property {string} href - URL of the linked resource\n * @property {string} method - HTTP method to use for the link\n */\n\n/**\n * @typedef {object} CreatePaymentRequest\n * @property {number} amount - Payment amount in pence\n * @property {string} reference - Unique reference for the payment\n * @property {string} description - Human-readable description of the payment\n * @property {string} return_url - URL to redirect the user to after payment\n * @property {boolean} [delayed_capture] - Whether to delay capturing the payment\n * @property {{ formId: string, slug: string }} [metadata] - Additional metadata for the payment\n */\n\n/**\n * @typedef {object} CreatePaymentResponse\n * @property {string} payment_id - Unique identifier for the created payment\n * @property {PaymentResponseState} state - Current state of the payment\n * @property {{ next_url: PaymentLink }} _links - HATEOAS links for the payment\n */\n\n/**\n * Base response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint\n * @typedef {object} GetPaymentResponseBase\n * @property {PaymentResponseState} state - Current state of the payment\n * @property {{ self: PaymentLink, next_url?: PaymentLink }} _links - HATEOAS links for the payment\n * @property {string} [email] - The paying user's email address\n */\n\n/**\n * Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint - not underscore in property name\n * @typedef {object} GetPaymentApiResponsePaymentProp\n * @property {string} payment_id - Unique identifier for the payment\n * @property {number} amount - amount of the payment\n */\n\n/**\n * Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint\n * @typedef {GetPaymentResponseBase & GetPaymentApiResponsePaymentProp} GetPaymentApiResponse\n */\n\n/**\n * Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse\n * @typedef {object} GetPaymentResponsePaymentProp\n * @property {string} paymentId - Unique identifier for the payment - note no underscore in property name\n * @property {number} amount - amount of the payment\n */\n\n/**\n * Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse\n * @typedef {GetPaymentResponseBase & GetPaymentResponsePaymentProp} GetPaymentResponse\n */\n\n/**\n * Payment session data stored when dispatching to GOV.UK Pay\n * @typedef {object} PaymentSessionData\n * @property {string} uuid - unique identifier for this payment attempt\n * @property {string} formId - id of the form\n * @property {string} reference - form reference number\n * @property {number} amount - amount in pounds\n * @property {string} description - payment description\n * @property {string} paymentId - GOV.UK Pay payment ID\n * @property {string} componentName - name of the PaymentField component\n * @property {string} returnUrl - URL to redirect to after successful payment\n * @property {string} failureUrl - URL to redirect to after failed/cancelled payment\n * @property {boolean} isLivePayment - whether the payment is using live API key\n */\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../../src/server/plugins/payment/types.js"],"sourcesContent":["/**\n * @typedef {object} PaymentResponseState\n * @property {'created' | 'started' | 'submitted' | 'capturable' | 'success' | 'failed' | 'cancelled' | 'error'} status - Current status of the payment\n * @property {boolean} finished - Whether the payment process has completed\n * @property {string} [message] - Human-readable message about the payment state\n * @property {string} [code] - Error or status code for the payment state\n */\n\n/**\n * @typedef {object} PaymentLink\n * @property {string} href - URL of the linked resource\n * @property {string} method - HTTP method to use for the link\n */\n\n/**\n * @typedef {object} CreatePaymentRequest\n * @property {number} amount - Payment amount in pence\n * @property {string} reference - Unique reference for the payment\n * @property {string} description - Human-readable description of the payment\n * @property {string} return_url - URL to redirect the user to after payment\n * @property {boolean} [delayed_capture] - Whether to delay capturing the payment\n * @property {{ formId: string, slug: string }} [metadata] - Additional metadata for the payment\n * @property {string} [email] - Email to prepopulate on GOV.UK Pay (max 254 chars)\n */\n\n/**\n * @typedef {object} CreatePaymentResponse\n * @property {string} payment_id - Unique identifier for the created payment\n * @property {PaymentResponseState} state - Current state of the payment\n * @property {{ next_url: PaymentLink }} _links - HATEOAS links for the payment\n */\n\n/**\n * Base response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint\n * @typedef {object} GetPaymentResponseBase\n * @property {PaymentResponseState} state - Current state of the payment\n * @property {{ self: PaymentLink, next_url?: PaymentLink }} _links - HATEOAS links for the payment\n * @property {string} [email] - The paying user's email address\n */\n\n/**\n * Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint - not underscore in property name\n * @typedef {object} GetPaymentApiResponsePaymentProp\n * @property {string} payment_id - Unique identifier for the payment\n * @property {number} amount - amount of the payment\n */\n\n/**\n * Response from GOV.UK Pay GET /v1/payments/{PAYMENT_ID} endpoint\n * @typedef {GetPaymentResponseBase & GetPaymentApiResponsePaymentProp} GetPaymentApiResponse\n */\n\n/**\n * Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse\n * @typedef {object} GetPaymentResponsePaymentProp\n * @property {string} paymentId - Unique identifier for the payment - note no underscore in property name\n * @property {number} amount - amount of the payment\n */\n\n/**\n * Response returned from getPaymentStatus - subtley different from GetPaymentApiResponse\n * @typedef {GetPaymentResponseBase & GetPaymentResponsePaymentProp} GetPaymentResponse\n */\n\n/**\n * Payment session data stored when dispatching to GOV.UK Pay\n * @typedef {object} PaymentSessionData\n * @property {string} uuid - unique identifier for this payment attempt\n * @property {string} formId - id of the form\n * @property {string} reference - form reference number\n * @property {number} amount - amount in pounds\n * @property {string} description - payment description\n * @property {string} paymentId - GOV.UK Pay payment ID\n * @property {string} componentName - name of the PaymentField component\n * @property {string} returnUrl - URL to redirect to after successful payment\n * @property {string} failureUrl - URL to redirect to after failed/cancelled payment\n * @property {boolean} isLivePayment - whether the payment is using live API key\n */\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "license": "SEE LICENSE IN LICENSE",
88
88
  "dependencies": {
89
- "@defra/forms-model": "^3.0.647",
89
+ "@defra/forms-model": "^3.0.648",
90
90
  "@defra/hapi-tracing": "^1.29.0",
91
91
  "@defra/interactive-map": "^0.0.17-alpha",
92
92
  "@elastic/ecs-pino-format": "^1.5.0",
@@ -0,0 +1,341 @@
1
+ ---
2
+ # Based on "Apply for a lock and weir fishing permit" production form.
3
+ # Extended with duration and site access to demonstrate complex compound conditions.
4
+ # Pricing matrix: duration x permit type x site access
5
+ schema: 2
6
+ name: Apply for a lock and weir fishing permit (v2 payment test)
7
+ engine: V2
8
+ declaration: I apply for permission to fish at the sites listed on this application for the duration of the permit, subject to the normal closed seasons.
9
+ startPage: '/fishing-sites'
10
+ options:
11
+ showReferenceNumber: true
12
+ pages:
13
+ - title: Fishing sites
14
+ path: '/fishing-sites'
15
+ components:
16
+ - id: '1fb84634-86f2-477b-affa-c7ace61aec26'
17
+ type: Markdown
18
+ content: "The fishing sites include:\n\n* Buscot\n* Grafton\n* Rushey\n* Sandford\n* Abingdon\n* Benson\n* Goring\n* Hurley\n* Bell Weir\n* Molesey"
19
+ options: {}
20
+ schema: {}
21
+ name: fishingSites
22
+ next: []
23
+ - title: Contact details
24
+ path: '/contact-details'
25
+ components:
26
+ - id: '3598ed25-1b9a-4ce6-8432-5676063b96ec'
27
+ type: TextField
28
+ title: What is your full name?
29
+ name: fullName
30
+ shortDescription: Full name
31
+ options:
32
+ required: true
33
+ schema: {}
34
+ - id: '9c4f0158-8f87-4cbd-a3a1-960166f015e4'
35
+ type: TelephoneNumberField
36
+ title: What is your phone number?
37
+ name: phoneNumber
38
+ shortDescription: Phone number
39
+ options:
40
+ required: true
41
+ schema: {}
42
+ - id: '9b83dc1e-e385-4cd3-b642-dcc247f3fc89'
43
+ type: EmailAddressField
44
+ title: What is your email address?
45
+ name: emailAddress
46
+ shortDescription: Email address
47
+ options:
48
+ required: true
49
+ next: []
50
+ - title: ''
51
+ path: '/rod-licence-number'
52
+ components:
53
+ - id: '22405838-becd-48ab-b984-c3c886822412'
54
+ type: TextField
55
+ title: What is your rod licence number (current or previous)?
56
+ name: rodLicenceNumber
57
+ shortDescription: Rod licence number
58
+ hint: The permit must be used in conjunction with a valid rod licence.
59
+ options:
60
+ required: true
61
+ schema: {}
62
+ next: []
63
+ - title: ''
64
+ path: '/permit-duration'
65
+ components:
66
+ - id: 'a1a1a1a1-1111-4aaa-aaaa-000000000001'
67
+ type: RadiosField
68
+ title: What duration of permit do you need?
69
+ name: permitDuration
70
+ shortDescription: Permit duration
71
+ options:
72
+ required: true
73
+ list: 'b1b1b1b1-1111-4bbb-bbbb-000000000001'
74
+ next: []
75
+ - title: ''
76
+ path: '/what-kind-of-permit-do-you-require'
77
+ components:
78
+ - id: 'f7663ac4-61e7-4b64-a157-70f002818493'
79
+ type: RadiosField
80
+ title: What kind of permit do you require?
81
+ name: permitType
82
+ shortDescription: Permit type
83
+ options:
84
+ required: true
85
+ list: 'aac4ee00-fb82-4a37-88ad-b9e10ded92e9'
86
+ next: []
87
+ - title: ''
88
+ path: '/site-access'
89
+ condition: 'c1c1c1c1-6666-4ccc-cccc-000000000005'
90
+ components:
91
+ - id: 'a1a1a1a1-1111-4aaa-aaaa-000000000003'
92
+ type: RadiosField
93
+ title: Which site access do you need?
94
+ name: siteAccess
95
+ shortDescription: Site access
96
+ hint: Single site permits are valid for one named site only.
97
+ options:
98
+ required: true
99
+ list: 'b1b1b1b1-1111-4bbb-bbbb-000000000002'
100
+ next: []
101
+ - title: ''
102
+ path: '/payment-required'
103
+ components:
104
+ - id: '6522e3f7-f414-42e5-9dbf-84e5868fbbd3'
105
+ type: PaymentField
106
+ title: Payment required
107
+ name: fishingPermitPayment
108
+ options:
109
+ required: true
110
+ amount: 0
111
+ description: Lock and weir fishing permit
112
+ emailField: emailAddress
113
+ conditionalAmounts:
114
+ # === 3-way: 12-month + type + site (most specific first) ===
115
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000001'
116
+ amount: 38
117
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000002'
118
+ amount: 25
119
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000003'
120
+ amount: 24
121
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000004'
122
+ amount: 16
123
+ # === 2-way: 8-day + type (site doesn't matter) ===
124
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000005'
125
+ amount: 12
126
+ - condition: 'd1d1d1d1-8888-4ddd-dddd-000000000006'
127
+ amount: 8
128
+ # === Simple: 1-day flat rate ===
129
+ - condition: 'c1c1c1c1-6666-4ccc-cccc-000000000003'
130
+ amount: 5
131
+ # === Simple: Junior free ===
132
+ - condition: 'c1c1c1c1-6666-4ccc-cccc-000000000004'
133
+ amount: 0
134
+ next: []
135
+ - title: ''
136
+ path: '/summary'
137
+ controller: SummaryPageController
138
+ conditions:
139
+ # =================================================================
140
+ # Simple atomic conditions
141
+ # =================================================================
142
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000001'
143
+ displayName: Is 12-month permit
144
+ items:
145
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000001'
146
+ componentId: 'a1a1a1a1-1111-4aaa-aaaa-000000000001'
147
+ operator: is
148
+ type: ListItemRef
149
+ value:
150
+ itemId: 'f1f1f1f1-1111-4fff-ffff-000000000001'
151
+ listId: 'b1b1b1b1-1111-4bbb-bbbb-000000000001'
152
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000002'
153
+ displayName: Is 8-day permit
154
+ items:
155
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000002'
156
+ componentId: 'a1a1a1a1-1111-4aaa-aaaa-000000000001'
157
+ operator: is
158
+ type: ListItemRef
159
+ value:
160
+ itemId: 'f1f1f1f1-1111-4fff-ffff-000000000002'
161
+ listId: 'b1b1b1b1-1111-4bbb-bbbb-000000000001'
162
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000003'
163
+ displayName: Is 1-day permit
164
+ items:
165
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000003'
166
+ componentId: 'a1a1a1a1-1111-4aaa-aaaa-000000000001'
167
+ operator: is
168
+ type: ListItemRef
169
+ value:
170
+ itemId: 'f1f1f1f1-1111-4fff-ffff-000000000003'
171
+ listId: 'b1b1b1b1-1111-4bbb-bbbb-000000000001'
172
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000004'
173
+ displayName: Is Junior permit type
174
+ items:
175
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000004'
176
+ componentId: 'f7663ac4-61e7-4b64-a157-70f002818493'
177
+ operator: is
178
+ type: ListItemRef
179
+ value:
180
+ itemId: 'f1f1f1f1-2222-4fff-ffff-000000000003'
181
+ listId: 'aac4ee00-fb82-4a37-88ad-b9e10ded92e9'
182
+ - id: 'dcaa3b3d-5cbc-4be9-b5ce-b2be5c72ccd1'
183
+ displayName: Is Adult permit type
184
+ items:
185
+ - id: '13b5e86b-2eb5-4a6c-8ab6-9fdc35907f18'
186
+ componentId: 'f7663ac4-61e7-4b64-a157-70f002818493'
187
+ operator: is
188
+ type: ListItemRef
189
+ value:
190
+ itemId: 'b6034236-63cf-44af-bb6c-d5d4d3825973'
191
+ listId: 'aac4ee00-fb82-4a37-88ad-b9e10ded92e9'
192
+ - id: 'c5177318-61ec-44ec-b5c2-18c1be1f1e42'
193
+ displayName: Is Concession permit type
194
+ items:
195
+ - id: 'fb75f1be-d471-4020-ac13-f7a2246078bb'
196
+ componentId: 'f7663ac4-61e7-4b64-a157-70f002818493'
197
+ operator: is
198
+ type: ListItemRef
199
+ value:
200
+ itemId: '86baf02e-0db7-4fad-8fce-fa9a30c1b7f0'
201
+ listId: 'aac4ee00-fb82-4a37-88ad-b9e10ded92e9'
202
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000007'
203
+ displayName: Is all sites access
204
+ items:
205
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000007'
206
+ componentId: 'a1a1a1a1-1111-4aaa-aaaa-000000000003'
207
+ operator: is
208
+ type: ListItemRef
209
+ value:
210
+ itemId: 'f1f1f1f1-3333-4fff-ffff-000000000001'
211
+ listId: 'b1b1b1b1-1111-4bbb-bbbb-000000000002'
212
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000008'
213
+ displayName: Is single site access
214
+ items:
215
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000008'
216
+ componentId: 'a1a1a1a1-1111-4aaa-aaaa-000000000003'
217
+ operator: is
218
+ type: ListItemRef
219
+ value:
220
+ itemId: 'f1f1f1f1-3333-4fff-ffff-000000000002'
221
+ listId: 'b1b1b1b1-1111-4bbb-bbbb-000000000002'
222
+ # =================================================================
223
+ # Page visibility: site access only shown for 12-month permits
224
+ # =================================================================
225
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000005'
226
+ displayName: Is 12-month (show site access page)
227
+ items:
228
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000005'
229
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000001'
230
+ # =================================================================
231
+ # 2-way compound AND: duration + type
232
+ # =================================================================
233
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000010'
234
+ displayName: 12-month AND Adult
235
+ coordinator: and
236
+ items:
237
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000010'
238
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000001'
239
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000011'
240
+ conditionId: 'dcaa3b3d-5cbc-4be9-b5ce-b2be5c72ccd1'
241
+ - id: 'c1c1c1c1-6666-4ccc-cccc-000000000011'
242
+ displayName: 12-month AND Concession
243
+ coordinator: and
244
+ items:
245
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000012'
246
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000001'
247
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000013'
248
+ conditionId: 'c5177318-61ec-44ec-b5c2-18c1be1f1e42'
249
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000005'
250
+ displayName: 8-day AND Adult
251
+ coordinator: and
252
+ items:
253
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000014'
254
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000002'
255
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000015'
256
+ conditionId: 'dcaa3b3d-5cbc-4be9-b5ce-b2be5c72ccd1'
257
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000006'
258
+ displayName: 8-day AND Concession
259
+ coordinator: and
260
+ items:
261
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000016'
262
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000002'
263
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000017'
264
+ conditionId: 'c5177318-61ec-44ec-b5c2-18c1be1f1e42'
265
+ # =================================================================
266
+ # 3-way compound AND: duration + type + site (chains 2-way refs)
267
+ # Same pattern as "Bats chargeable use" in protected species form
268
+ # =================================================================
269
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000001'
270
+ displayName: 12-month AND Adult AND All sites
271
+ coordinator: and
272
+ items:
273
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000020'
274
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000010'
275
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000021'
276
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000007'
277
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000002'
278
+ displayName: 12-month AND Adult AND Single site
279
+ coordinator: and
280
+ items:
281
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000022'
282
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000010'
283
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000023'
284
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000008'
285
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000003'
286
+ displayName: 12-month AND Concession AND All sites
287
+ coordinator: and
288
+ items:
289
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000024'
290
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000011'
291
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000025'
292
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000007'
293
+ - id: 'd1d1d1d1-8888-4ddd-dddd-000000000004'
294
+ displayName: 12-month AND Concession AND Single site
295
+ coordinator: and
296
+ items:
297
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000026'
298
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000011'
299
+ - id: 'e1e1e1e1-7777-4eee-eeee-000000000027'
300
+ conditionId: 'c1c1c1c1-6666-4ccc-cccc-000000000008'
301
+ sections: []
302
+ lists:
303
+ - id: 'b1b1b1b1-1111-4bbb-bbbb-000000000001'
304
+ title: Permit durations
305
+ name: permitDurations
306
+ type: string
307
+ items:
308
+ - id: 'f1f1f1f1-1111-4fff-ffff-000000000001'
309
+ text: 12-month permit
310
+ value: 12-month
311
+ - id: 'f1f1f1f1-1111-4fff-ffff-000000000002'
312
+ text: 8-day permit
313
+ value: 8-day
314
+ - id: 'f1f1f1f1-1111-4fff-ffff-000000000003'
315
+ text: 1-day permit
316
+ value: 1-day
317
+ - id: 'aac4ee00-fb82-4a37-88ad-b9e10ded92e9'
318
+ title: Permit types
319
+ name: permitTypes
320
+ type: string
321
+ items:
322
+ - id: 'b6034236-63cf-44af-bb6c-d5d4d3825973'
323
+ text: Adult
324
+ value: Adult
325
+ - id: '86baf02e-0db7-4fad-8fce-fa9a30c1b7f0'
326
+ text: Concession (65+ or disabled)
327
+ value: Concession
328
+ - id: 'f1f1f1f1-2222-4fff-ffff-000000000003'
329
+ text: Junior (13-16 years)
330
+ value: Junior
331
+ - id: 'b1b1b1b1-1111-4bbb-bbbb-000000000002'
332
+ title: Site access
333
+ name: siteAccess
334
+ type: string
335
+ items:
336
+ - id: 'f1f1f1f1-3333-4fff-ffff-000000000001'
337
+ text: All sites
338
+ value: all-sites
339
+ - id: 'f1f1f1f1-3333-4fff-ffff-000000000002'
340
+ text: Single site
341
+ value: single-site
@@ -7,6 +7,7 @@ import {
7
7
  import { StatusCodes } from 'http-status-codes'
8
8
  import joi, { type ObjectSchema } from 'joi'
9
9
 
10
+ import { createLogger } from '../../../common/helpers/logging/logger.js'
10
11
  import { COMPONENT_STATE_ERROR } from '../../../constants.js'
11
12
  import { FormComponent } from './FormComponent.js'
12
13
  import { type PaymentState } from './PaymentField.types.js'
@@ -14,6 +15,7 @@ import {
14
15
  createError,
15
16
  getPluginOptions
16
17
  } from '../helpers.js'
18
+ import { type FormModel } from '../models/index.js'
17
19
  import {
18
20
  PaymentErrorTypes,
19
21
  PaymentPreAuthError,
@@ -38,6 +40,8 @@ import {
38
40
  formatCurrency
39
41
  } from '../../payment/helper.js'
40
42
 
43
+ const logger = createLogger()
44
+
41
45
  export class PaymentField extends FormComponent {
42
46
  declare options: PaymentFieldComponent['options']
43
47
  declare formSchema: ObjectSchema
@@ -109,7 +113,9 @@ export class PaymentField extends FormComponent {
109
113
  ? (payload[this.name] as unknown as PaymentState)
110
114
  : undefined
111
115
 
112
- // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.
116
+ // Use payment state amount if pre-authorized, otherwise use default.
117
+ // The page controller overrides this with the resolved conditional amount
118
+ // using the full form state (which getViewModel doesn't have access to).
113
119
  const amount = paymentState?.amount ?? this.options.amount
114
120
 
115
121
  return {
@@ -184,6 +190,37 @@ export class PaymentField extends FormComponent {
184
190
  }
185
191
  }
186
192
 
193
+ /**
194
+ * Resolves the payment amount from conditional amounts configuration.
195
+ * Evaluates conditions in order; first true condition wins.
196
+ * Falls back to the default options.amount.
197
+ */
198
+ static resolveAmount(
199
+ options: PaymentFieldComponent['options'],
200
+ model: FormModel,
201
+ state: FormState
202
+ ): number {
203
+ const { conditionalAmounts } = options
204
+
205
+ if (!conditionalAmounts?.length) {
206
+ return options.amount
207
+ }
208
+
209
+ for (const { condition, amount } of conditionalAmounts) {
210
+ if (!model.conditions[condition]) {
211
+ logger.warn(
212
+ `[payment] Condition '${condition}' not found in form conditions. Skipping.`
213
+ )
214
+ continue
215
+ }
216
+ if (model.conditions[condition].fn(state)) {
217
+ return amount
218
+ }
219
+ }
220
+
221
+ return options.amount
222
+ }
223
+
187
224
  /**
188
225
  * Dispatcher for external redirect to GOV.UK Pay
189
226
  */
@@ -219,7 +256,12 @@ export class PaymentField extends FormComponent {
219
256
  const uuid = randomUUID()
220
257
 
221
258
  const reference = state.$$__referenceNumber as string
222
- const amount = options.amount
259
+ const resolvedAmount = PaymentField.resolveAmount(options, model, state)
260
+
261
+ // Zero-amount safety net (page skip should prevent this, but defensive)
262
+ if (resolvedAmount === 0) {
263
+ return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)
264
+ }
223
265
 
224
266
  const description = options.description
225
267
 
@@ -228,14 +270,26 @@ export class PaymentField extends FormComponent {
228
270
  const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`
229
271
  const paymentPageUrl = args.sourceUrl
230
272
 
231
- const amountInPence = Math.round(amount * 100)
273
+ // Prepopulate GOV.UK Pay email if emailField is configured.
274
+ // The referenced EmailAddressField validates with joi.string().email()
275
+ // at input time, so the value in state is already validated.
276
+ let prefilledEmail: string | undefined
277
+ if (options.emailField) {
278
+ const emailValue = state[options.emailField]
279
+ if (typeof emailValue === 'string' && emailValue) {
280
+ prefilledEmail = emailValue
281
+ }
282
+ }
283
+
284
+ const amountInPence = Math.round(resolvedAmount * 100)
232
285
  const payment = await paymentService.createPayment(
233
286
  amountInPence,
234
287
  description,
235
288
  payCallbackUrl,
236
289
  reference,
237
290
  isLivePayment,
238
- { formId, slug }
291
+ { formId, slug },
292
+ prefilledEmail
239
293
  )
240
294
 
241
295
  if (!payment) {
@@ -253,7 +307,7 @@ export class PaymentField extends FormComponent {
253
307
  uuid,
254
308
  formId,
255
309
  reference,
256
- amount,
310
+ amount: resolvedAmount,
257
311
  description,
258
312
  paymentId: payment.paymentId,
259
313
  componentName,
@@ -276,6 +330,16 @@ export class PaymentField extends FormComponent {
276
330
  _metadata: FormMetadata,
277
331
  context: FormContext
278
332
  ): Promise<void> {
333
+ // Zero-amount bypass — no capture needed
334
+ const resolvedAmount = PaymentField.resolveAmount(
335
+ this.options,
336
+ this.model,
337
+ context.state
338
+ )
339
+ if (resolvedAmount === 0) {
340
+ return
341
+ }
342
+
279
343
  const paymentState = this.getPaymentStateFromState(context.state)
280
344
 
281
345
  if (!paymentState) {
@@ -309,7 +373,7 @@ export class PaymentField extends FormComponent {
309
373
 
310
374
  PaymentSubmissionError.checkPaymentAmount(
311
375
  status.amount,
312
- this.options.amount,
376
+ resolvedAmount,
313
377
  this
314
378
  )
315
379
 
@@ -56,6 +56,8 @@ export class SummaryViewModel {
56
56
  allowSaveAndExit = false
57
57
  paymentState?: PaymentState
58
58
  paymentDetails?: CheckAnswers
59
+ paymentRequired?: boolean
60
+ paymentPreAuthorized?: boolean
59
61
 
60
62
  constructor(
61
63
  request: FormContextRequest,