@defra/forms-engine-plugin 4.0.51 → 4.0.52

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 (25) hide show
  1. package/.server/server/forms/payment-test.yaml +1 -1
  2. package/.server/server/plugins/engine/components/PaymentField.js +4 -5
  3. package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
  4. package/.server/server/plugins/engine/outputFormatters/human/v1.js +2 -2
  5. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  6. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -2
  7. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  8. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +2 -2
  9. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  10. package/.server/server/plugins/engine/views/components/paymentfield.html +1 -1
  11. package/.server/server/plugins/payment/helper.d.ts +5 -3
  12. package/.server/server/plugins/payment/helper.js +10 -4
  13. package/.server/server/plugins/payment/helper.js.map +1 -1
  14. package/.server/server/plugins/payment/helper.test.js +12 -6
  15. package/.server/server/plugins/payment/helper.test.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/server/forms/payment-test.yaml +1 -1
  18. package/src/server/plugins/engine/components/PaymentField.test.ts +2 -2
  19. package/src/server/plugins/engine/components/PaymentField.ts +7 -6
  20. package/src/server/plugins/engine/outputFormatters/human/v1.ts +2 -2
  21. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +2 -2
  22. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +2 -2
  23. package/src/server/plugins/engine/views/components/paymentfield.html +1 -1
  24. package/src/server/plugins/payment/helper.js +11 -4
  25. package/src/server/plugins/payment/helper.test.js +14 -6
@@ -27,7 +27,7 @@ pages:
27
27
  type: PaymentField
28
28
  options:
29
29
  required: true
30
- amount: 300
30
+ amount: 10000
31
31
  description: Processing fee for your application.
32
32
  next:
33
33
  - path: '/summary'
@@ -4,7 +4,7 @@ import joi from 'joi';
4
4
  import { FormComponent } from "./FormComponent.js";
5
5
  import { getPluginOptions } from "../helpers.js";
6
6
  import { PaymentErrorTypes, PaymentPreAuthError, PaymentSubmissionError } from "../pageControllers/errors.js";
7
- import { createPaymentService } from "../../payment/helper.js";
7
+ import { createPaymentService, formatCurrency } from "../../payment/helper.js";
8
8
  export class PaymentField extends FormComponent {
9
9
  isAppendageStateSingleObject = true;
10
10
  constructor(def, props) {
@@ -42,7 +42,7 @@ export class PaymentField extends FormComponent {
42
42
  if (!value) {
43
43
  return '';
44
44
  }
45
- return `£${value.amount.toFixed(2)} - ${value.description}`;
45
+ return `${formatCurrency(value.amount)} - ${value.description}`;
46
46
  }
47
47
  getViewModel(payload, errors) {
48
48
  const viewModel = super.getViewModel(payload, errors);
@@ -52,10 +52,9 @@ export class PaymentField extends FormComponent {
52
52
 
53
53
  // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.
54
54
  const amount = paymentState?.amount ?? this.options.amount;
55
- const formattedAmount = amount.toFixed(2);
56
55
  return {
57
56
  ...viewModel,
58
- amount: formattedAmount,
57
+ amount: formatCurrency(amount),
59
58
  description: this.options.description,
60
59
  paymentState
61
60
  };
@@ -89,7 +88,7 @@ export class PaymentField extends FormComponent {
89
88
  return this.isPaymentState(value) ? value : undefined;
90
89
  }
91
90
  getContextValueFromState(state) {
92
- return this.isPaymentState(state) ? `Reference: ${state.reference}\nAmount: ${state.amount.toFixed(2)}` : '';
91
+ return this.isPaymentState(state) ? `Reference: ${state.reference}\nAmount: ${formatCurrency(state.amount)}` : '';
93
92
  }
94
93
 
95
94
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"PaymentField.js","names":["randomUUID","StatusCodes","joi","FormComponent","getPluginOptions","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","createPaymentService","PaymentField","isAppendageStateSingleObject","constructor","def","props","options","paymentStateSchema","object","paymentId","string","required","reference","amount","number","description","uuid","formId","isLivePayment","boolean","preAuth","status","valid","createdAt","isoDate","unknown","label","formSchema","stateSchema","getPaymentStateFromState","state","value","name","isPaymentState","undefined","getDisplayStringFromState","toFixed","getViewModel","payload","errors","viewModel","paymentState","formattedAmount","Array","isArray","isState","getFormValue","getContextValueFromState","getAllPossibleErrors","baseErrors","type","template","advancedSettingsErrors","dispatcher","request","h","args","componentName","component","model","controller","getState","baseUrl","server","summaryUrl","basePath","existingPaymentState","redirect","code","SEE_OTHER","isLive","isPreview","paymentService","$$__referenceNumber","slug","payCallbackUrl","paymentPageUrl","sourceUrl","amountInPence","Math","round","payment","createPayment","sessionData","returnUrl","failureUrl","yar","set","paymentUrl","onSubmit","_metadata","context","PaymentIncomplete","capture","getPaymentStatus","checkPaymentAmount","markPaymentCaptured","PaymentExpired","captured","capturePayment","updatedState","Date","toISOString","page","currentState","mergeState"],"sources":["../../../../../src/server/plugins/engine/components/PaymentField.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nimport {\n type FormMetadata,\n type PaymentFieldComponent\n} from '@defra/forms-model'\nimport { StatusCodes } from 'http-status-codes'\nimport joi, { type ObjectSchema } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'\nimport {\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/plugins/engine/types/index.js'\nimport {\n type ErrorMessageTemplateList,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { createPaymentService } from '~/src/server/plugins/payment/helper.js'\n\nexport class PaymentField extends FormComponent {\n declare options: PaymentFieldComponent['options']\n declare formSchema: ObjectSchema\n declare stateSchema: ObjectSchema\n isAppendageStateSingleObject = true\n\n constructor(\n def: PaymentFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n this.options = def.options\n\n const paymentStateSchema = joi\n .object({\n paymentId: joi.string().required(),\n reference: joi.string().required(),\n amount: joi.number().required(),\n description: joi.string().required(),\n uuid: joi.string().uuid().required(),\n formId: joi.string().required(),\n isLivePayment: joi.boolean().required(),\n preAuth: joi\n .object({\n status: joi\n .string()\n .valid('success', 'failed', 'started')\n .required(),\n createdAt: joi.string().isoDate().required()\n })\n .required()\n })\n .unknown(true)\n .label(this.label)\n\n this.formSchema = paymentStateSchema\n // 'required()' forces the payment page to be invalid until we have valid payment state\n // i.e. the user will automatically be directed back to the payment page\n // if they attempt to access future pages when no payment entered yet\n this.stateSchema = paymentStateSchema.required()\n }\n\n /**\n * Gets the PaymentState from form submission state\n */\n getPaymentStateFromState(\n state: FormSubmissionState\n ): PaymentState | undefined {\n const value = state[this.name]\n return this.isPaymentState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState): string {\n const value = this.getPaymentStateFromState(state)\n\n if (!value) {\n return ''\n }\n\n return `£${value.amount.toFixed(2)} - ${value.description}`\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n\n // Payload is pre-populated from state if a payment has already been made\n const paymentState = this.isPaymentState(payload[this.name] as unknown)\n ? (payload[this.name] as unknown as PaymentState)\n : undefined\n\n // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.\n const amount = paymentState?.amount ?? this.options.amount\n\n const formattedAmount = amount.toFixed(2)\n\n return {\n ...viewModel,\n amount: formattedAmount,\n description: this.options.description,\n paymentState\n }\n }\n\n /**\n * Type guard to check if value is PaymentState\n */\n isPaymentState(value: unknown): value is PaymentState {\n return PaymentField.isPaymentState(value)\n }\n\n /**\n * Static type guard to check if value is PaymentState\n */\n static isPaymentState(value: unknown): value is PaymentState {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false\n }\n\n const state = value as PaymentState\n return (\n typeof state.paymentId === 'string' &&\n typeof state.amount === 'number' &&\n typeof state.description === 'string'\n )\n }\n\n /**\n * Override base isState to validate PaymentState\n */\n isState(value?: FormStateValue | FormState): value is FormState {\n return this.isPaymentState(value)\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isPaymentState(value)\n ? (value as unknown as NonNullable<FormStateValue>)\n : undefined\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n return this.isPaymentState(state)\n ? `Reference: ${state.reference}\\nAmount: ${state.amount.toFixed(2)}`\n : ''\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return PaymentField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n {\n type: 'paymentRequired',\n template: 'Complete the payment to continue'\n }\n ],\n advancedSettingsErrors: []\n }\n }\n\n /**\n * Dispatcher for external redirect to GOV.UK Pay\n */\n static async dispatcher(\n request: FormRequestPayload,\n h: FormResponseToolkit,\n args: PaymentDispatcherArgs\n ): Promise<unknown> {\n const { options, name: componentName } = args.component\n const { model } = args.controller\n\n const state = await args.controller.getState(request)\n const { baseUrl } = getPluginOptions(request.server)\n const summaryUrl = `${baseUrl}/${model.basePath}/summary`\n\n const existingPaymentState = state[componentName]\n if (\n PaymentField.isPaymentState(existingPaymentState) &&\n existingPaymentState.preAuth?.status === 'success'\n ) {\n return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)\n }\n\n const isLivePayment = args.isLive && !args.isPreview\n const formId = args.controller.model.formId\n const paymentService = createPaymentService(isLivePayment, formId)\n\n const uuid = randomUUID()\n\n const reference = state.$$__referenceNumber as string\n const amount = options.amount\n\n const description = options.description\n\n const slug = `/${model.basePath}`\n\n const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`\n const paymentPageUrl = args.sourceUrl\n\n const amountInPence = Math.round(amount * 100)\n const payment = await paymentService.createPayment(\n amountInPence,\n description,\n payCallbackUrl,\n reference,\n { formId, slug }\n )\n\n const sessionData: PaymentSessionData = {\n uuid,\n formId,\n reference,\n amount,\n description,\n paymentId: payment.paymentId,\n componentName,\n returnUrl: summaryUrl,\n failureUrl: paymentPageUrl,\n isLivePayment\n }\n\n request.yar.set(`payment-${uuid}`, sessionData)\n\n return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)\n }\n\n /**\n * Called on form submission to capture the payment\n * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment\n */\n async onSubmit(\n request: FormRequestPayload,\n _metadata: FormMetadata,\n context: FormContext\n ): Promise<void> {\n const paymentState = this.getPaymentStateFromState(context.state)\n\n if (!paymentState) {\n throw new PaymentPreAuthError(\n this,\n 'Complete the payment to continue',\n true,\n PaymentErrorTypes.PaymentIncomplete\n )\n }\n\n if (paymentState.capture?.status === 'success') {\n return\n }\n\n const { paymentId, isLivePayment, formId } = paymentState\n const paymentService = createPaymentService(isLivePayment, formId)\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const status = await paymentService.getPaymentStatus(paymentId)\n\n PaymentSubmissionError.checkPaymentAmount(\n status.amount,\n this.options.amount,\n this\n )\n\n if (status.state.status === 'success') {\n await this.markPaymentCaptured(request, paymentState)\n return\n }\n\n if (status.state.status !== 'capturable') {\n throw new PaymentPreAuthError(\n this,\n 'Your payment authorisation has expired. Please add your payment details again.',\n true,\n PaymentErrorTypes.PaymentExpired\n )\n }\n\n const captured = await paymentService.capturePayment(\n paymentId,\n status.amount\n )\n\n if (!captured) {\n throw new PaymentPreAuthError(\n this,\n 'There was a problem and your form was not submitted. Try submitting the form again.',\n false\n )\n }\n\n await this.markPaymentCaptured(request, paymentState)\n }\n\n /**\n * Updates payment state to mark capture as successful\n * This ensures we don't try to re-capture on submission retry\n */\n private async markPaymentCaptured(\n request: FormRequestPayload,\n paymentState: PaymentState\n ): Promise<void> {\n const updatedState: PaymentState = {\n ...paymentState,\n capture: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n if (this.page) {\n const currentState = await this.page.getState(request)\n await this.page.mergeState(request, currentState, {\n [this.name]: updatedState\n })\n }\n }\n}\n\nexport interface PaymentDispatcherArgs {\n controller: {\n model: {\n formId: string\n basePath: string\n name: string\n }\n getState: (request: AnyFormRequest) => Promise<FormSubmissionState>\n }\n component: PaymentField\n sourceUrl: string\n isLive: boolean\n isPreview: boolean\n}\n\n/**\n * Session data stored when dispatching to GOV.UK Pay\n */\nexport interface PaymentSessionData {\n uuid: string\n formId: string\n reference: string\n amount: number\n description: string\n paymentId: string\n componentName: string\n returnUrl: string\n failureUrl: string\n isLivePayment: boolean\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"PaymentField.js","names":["randomUUID","StatusCodes","joi","FormComponent","getPluginOptions","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","createPaymentService","formatCurrency","PaymentField","isAppendageStateSingleObject","constructor","def","props","options","paymentStateSchema","object","paymentId","string","required","reference","amount","number","description","uuid","formId","isLivePayment","boolean","preAuth","status","valid","createdAt","isoDate","unknown","label","formSchema","stateSchema","getPaymentStateFromState","state","value","name","isPaymentState","undefined","getDisplayStringFromState","getViewModel","payload","errors","viewModel","paymentState","Array","isArray","isState","getFormValue","getContextValueFromState","getAllPossibleErrors","baseErrors","type","template","advancedSettingsErrors","dispatcher","request","h","args","componentName","component","model","controller","getState","baseUrl","server","summaryUrl","basePath","existingPaymentState","redirect","code","SEE_OTHER","isLive","isPreview","paymentService","$$__referenceNumber","slug","payCallbackUrl","paymentPageUrl","sourceUrl","amountInPence","Math","round","payment","createPayment","sessionData","returnUrl","failureUrl","yar","set","paymentUrl","onSubmit","_metadata","context","PaymentIncomplete","capture","getPaymentStatus","checkPaymentAmount","markPaymentCaptured","PaymentExpired","captured","capturePayment","updatedState","Date","toISOString","page","currentState","mergeState"],"sources":["../../../../../src/server/plugins/engine/components/PaymentField.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nimport {\n type FormMetadata,\n type PaymentFieldComponent\n} from '@defra/forms-model'\nimport { StatusCodes } from 'http-status-codes'\nimport joi, { type ObjectSchema } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'\nimport {\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/plugins/engine/types/index.js'\nimport {\n type ErrorMessageTemplateList,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport {\n createPaymentService,\n formatCurrency\n} from '~/src/server/plugins/payment/helper.js'\n\nexport class PaymentField extends FormComponent {\n declare options: PaymentFieldComponent['options']\n declare formSchema: ObjectSchema\n declare stateSchema: ObjectSchema\n isAppendageStateSingleObject = true\n\n constructor(\n def: PaymentFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n this.options = def.options\n\n const paymentStateSchema = joi\n .object({\n paymentId: joi.string().required(),\n reference: joi.string().required(),\n amount: joi.number().required(),\n description: joi.string().required(),\n uuid: joi.string().uuid().required(),\n formId: joi.string().required(),\n isLivePayment: joi.boolean().required(),\n preAuth: joi\n .object({\n status: joi\n .string()\n .valid('success', 'failed', 'started')\n .required(),\n createdAt: joi.string().isoDate().required()\n })\n .required()\n })\n .unknown(true)\n .label(this.label)\n\n this.formSchema = paymentStateSchema\n // 'required()' forces the payment page to be invalid until we have valid payment state\n // i.e. the user will automatically be directed back to the payment page\n // if they attempt to access future pages when no payment entered yet\n this.stateSchema = paymentStateSchema.required()\n }\n\n /**\n * Gets the PaymentState from form submission state\n */\n getPaymentStateFromState(\n state: FormSubmissionState\n ): PaymentState | undefined {\n const value = state[this.name]\n return this.isPaymentState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState): string {\n const value = this.getPaymentStateFromState(state)\n\n if (!value) {\n return ''\n }\n\n return `${formatCurrency(value.amount)} - ${value.description}`\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n\n // Payload is pre-populated from state if a payment has already been made\n const paymentState = this.isPaymentState(payload[this.name] as unknown)\n ? (payload[this.name] as unknown as PaymentState)\n : undefined\n\n // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.\n const amount = paymentState?.amount ?? this.options.amount\n\n return {\n ...viewModel,\n amount: formatCurrency(amount),\n description: this.options.description,\n paymentState\n }\n }\n\n /**\n * Type guard to check if value is PaymentState\n */\n isPaymentState(value: unknown): value is PaymentState {\n return PaymentField.isPaymentState(value)\n }\n\n /**\n * Static type guard to check if value is PaymentState\n */\n static isPaymentState(value: unknown): value is PaymentState {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false\n }\n\n const state = value as PaymentState\n return (\n typeof state.paymentId === 'string' &&\n typeof state.amount === 'number' &&\n typeof state.description === 'string'\n )\n }\n\n /**\n * Override base isState to validate PaymentState\n */\n isState(value?: FormStateValue | FormState): value is FormState {\n return this.isPaymentState(value)\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isPaymentState(value)\n ? (value as unknown as NonNullable<FormStateValue>)\n : undefined\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n return this.isPaymentState(state)\n ? `Reference: ${state.reference}\\nAmount: ${formatCurrency(state.amount)}`\n : ''\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return PaymentField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n {\n type: 'paymentRequired',\n template: 'Complete the payment to continue'\n }\n ],\n advancedSettingsErrors: []\n }\n }\n\n /**\n * Dispatcher for external redirect to GOV.UK Pay\n */\n static async dispatcher(\n request: FormRequestPayload,\n h: FormResponseToolkit,\n args: PaymentDispatcherArgs\n ): Promise<unknown> {\n const { options, name: componentName } = args.component\n const { model } = args.controller\n\n const state = await args.controller.getState(request)\n const { baseUrl } = getPluginOptions(request.server)\n const summaryUrl = `${baseUrl}/${model.basePath}/summary`\n\n const existingPaymentState = state[componentName]\n if (\n PaymentField.isPaymentState(existingPaymentState) &&\n existingPaymentState.preAuth?.status === 'success'\n ) {\n return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)\n }\n\n const isLivePayment = args.isLive && !args.isPreview\n const formId = args.controller.model.formId\n const paymentService = createPaymentService(isLivePayment, formId)\n\n const uuid = randomUUID()\n\n const reference = state.$$__referenceNumber as string\n const amount = options.amount\n\n const description = options.description\n\n const slug = `/${model.basePath}`\n\n const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`\n const paymentPageUrl = args.sourceUrl\n\n const amountInPence = Math.round(amount * 100)\n const payment = await paymentService.createPayment(\n amountInPence,\n description,\n payCallbackUrl,\n reference,\n { formId, slug }\n )\n\n const sessionData: PaymentSessionData = {\n uuid,\n formId,\n reference,\n amount,\n description,\n paymentId: payment.paymentId,\n componentName,\n returnUrl: summaryUrl,\n failureUrl: paymentPageUrl,\n isLivePayment\n }\n\n request.yar.set(`payment-${uuid}`, sessionData)\n\n return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)\n }\n\n /**\n * Called on form submission to capture the payment\n * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment\n */\n async onSubmit(\n request: FormRequestPayload,\n _metadata: FormMetadata,\n context: FormContext\n ): Promise<void> {\n const paymentState = this.getPaymentStateFromState(context.state)\n\n if (!paymentState) {\n throw new PaymentPreAuthError(\n this,\n 'Complete the payment to continue',\n true,\n PaymentErrorTypes.PaymentIncomplete\n )\n }\n\n if (paymentState.capture?.status === 'success') {\n return\n }\n\n const { paymentId, isLivePayment, formId } = paymentState\n const paymentService = createPaymentService(isLivePayment, formId)\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const status = await paymentService.getPaymentStatus(paymentId)\n\n PaymentSubmissionError.checkPaymentAmount(\n status.amount,\n this.options.amount,\n this\n )\n\n if (status.state.status === 'success') {\n await this.markPaymentCaptured(request, paymentState)\n return\n }\n\n if (status.state.status !== 'capturable') {\n throw new PaymentPreAuthError(\n this,\n 'Your payment authorisation has expired. Please add your payment details again.',\n true,\n PaymentErrorTypes.PaymentExpired\n )\n }\n\n const captured = await paymentService.capturePayment(\n paymentId,\n status.amount\n )\n\n if (!captured) {\n throw new PaymentPreAuthError(\n this,\n 'There was a problem and your form was not submitted. Try submitting the form again.',\n false\n )\n }\n\n await this.markPaymentCaptured(request, paymentState)\n }\n\n /**\n * Updates payment state to mark capture as successful\n * This ensures we don't try to re-capture on submission retry\n */\n private async markPaymentCaptured(\n request: FormRequestPayload,\n paymentState: PaymentState\n ): Promise<void> {\n const updatedState: PaymentState = {\n ...paymentState,\n capture: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n if (this.page) {\n const currentState = await this.page.getState(request)\n await this.page.mergeState(request, currentState, {\n [this.name]: updatedState\n })\n }\n }\n}\n\nexport interface PaymentDispatcherArgs {\n controller: {\n model: {\n formId: string\n basePath: string\n name: string\n }\n getState: (request: AnyFormRequest) => Promise<FormSubmissionState>\n }\n component: PaymentField\n sourceUrl: string\n isLive: boolean\n isPreview: boolean\n}\n\n/**\n * Session data stored when dispatching to GOV.UK Pay\n */\nexport interface PaymentSessionData {\n uuid: string\n formId: string\n reference: string\n amount: number\n description: string\n paymentId: string\n componentName: string\n returnUrl: string\n failureUrl: string\n isLivePayment: boolean\n}\n"],"mappings":"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","ignoreList":[]}
@@ -3,7 +3,7 @@ import { config } from "../../../../../config/index.js";
3
3
  import { getAnswer } from "../../components/helpers/components.js";
4
4
  import { escapeMarkdown } from "../../components/helpers/index.js";
5
5
  import { PaymentField } from "../../components/index.js";
6
- import { formatPaymentAmount, formatPaymentDate } from "../../../payment/helper.js";
6
+ import { formatCurrency, formatPaymentDate } from "../../../payment/helper.js";
7
7
  const designerUrl = config.get('designerUrl');
8
8
  export function format(_context, items, model, submitResponse, formStatus, _formMetadata) {
9
9
  const {
@@ -71,7 +71,7 @@ function appendPaymentSection(paymentItems, lines) {
71
71
  if (!paymentState) {
72
72
  return;
73
73
  }
74
- const formattedAmount = formatPaymentAmount(paymentState.amount);
74
+ const formattedAmount = formatCurrency(paymentState.amount);
75
75
  const dateOfPayment = paymentState.preAuth?.createdAt ? formatPaymentDate(paymentState.preAuth.createdAt) : '';
76
76
  lines.push('---\n', `# Your payment of ${formattedAmount} was successful\n`, '## Payment for\n', `${escapeMarkdown(paymentState.description)}\n`, '---\n', '## Total amount\n', `${formattedAmount}\n`, '---\n', '## Date of payment\n', `${escapeMarkdown(dateOfPayment)}\n`, '---\n');
77
77
  }
@@ -1 +1 @@
1
- {"version":3,"file":"v1.js","names":["addDays","format","dateFormat","config","getAnswer","escapeMarkdown","PaymentField","formatPaymentAmount","formatPaymentDate","designerUrl","get","_context","items","model","submitResponse","formStatus","_formMetadata","files","result","formName","name","now","Date","formattedNow","fileExpiryDate","formattedExpiryDate","lines","push","isPreview","state","regularItems","filter","item","isPaymentItem","paymentItems","forEach","label","filename","fileId","repeaters","field","main","appendPaymentSection","join","length","paymentItem","paymentField","paymentState","getPaymentStateFromState","formattedAmount","amount","dateOfPayment","preAuth","createdAt","description"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/human/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\nimport { addDays, format as dateFormat } from 'date-fns'\n\nimport { config } from '~/src/config/index.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/index.js'\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n formatPaymentAmount,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nconst designerUrl = config.get('designerUrl')\n\nexport function format(\n _context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n _formMetadata?: FormMetadata\n) {\n const { files } = submitResponse.result\n\n const formName = escapeMarkdown(model.name)\n\n /**\n * @todo Refactor this below but the code to\n * generate the question and answers works for now\n */\n const now = new Date()\n const formattedNow = `${dateFormat(now, 'h:mmaaa')} on ${dateFormat(now, 'd MMMM yyyy')}`\n\n const fileExpiryDate = addDays(now, 90)\n const formattedExpiryDate = `${dateFormat(fileExpiryDate, 'h:mmaaa')} on ${dateFormat(fileExpiryDate, 'eeee d MMMM yyyy')}`\n\n const lines: string[] = []\n\n lines.push(\n `^ For security reasons, the links in this email expire at ${escapeMarkdown(formattedExpiryDate)}\\n`\n )\n\n if (formStatus.isPreview) {\n lines.push(`This is a test of the ${formName} ${formStatus.state} form.\\n`)\n }\n\n lines.push(`${formName} form received at ${escapeMarkdown(formattedNow)}.\\n`)\n lines.push('---\\n')\n\n const regularItems = items.filter((item) => !isPaymentItem(item))\n const paymentItems = items.filter((item) => isPaymentItem(item))\n\n regularItems.forEach((item) => {\n const label = escapeMarkdown(item.label)\n\n lines.push(`## ${label}\\n`)\n\n if ('subItems' in item) {\n const filename = escapeMarkdown(`Download ${label} (CSV)`)\n const fileId = files.repeaters[item.name]\n\n lines.push(`[${filename}](${designerUrl}/file-download/${fileId})\\n`)\n } else {\n lines.push(\n getAnswer(item.field, item.state, {\n format: 'email'\n })\n )\n }\n\n lines.push('---\\n')\n })\n\n const filename = escapeMarkdown('Download main form (CSV)')\n lines.push(`[${filename}](${designerUrl}/file-download/${files.main})\\n`)\n\n appendPaymentSection(paymentItems, lines)\n\n return lines.join('\\n')\n}\n\n/**\n * Check if an item is a PaymentField\n */\nfunction isPaymentItem(item: DetailItem): boolean {\n if ('subItems' in item) {\n return false\n }\n return item.field instanceof PaymentField\n}\n\n/**\n * Appends the payment details section to the email lines if payment exists\n */\nfunction appendPaymentSection(paymentItems: DetailItem[], lines: string[]) {\n if (paymentItems.length === 0) {\n return\n }\n\n const paymentItem = paymentItems[0] as DetailItemField\n const paymentField = paymentItem.field as PaymentField\n const paymentState = paymentField.getPaymentStateFromState(paymentItem.state)\n\n if (!paymentState) {\n return\n }\n\n const formattedAmount = formatPaymentAmount(paymentState.amount)\n const dateOfPayment = paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n\n lines.push(\n '---\\n',\n `# Your payment of ${formattedAmount} was successful\\n`,\n '## Payment for\\n',\n `${escapeMarkdown(paymentState.description)}\\n`,\n '---\\n',\n '## Total amount\\n',\n `${formattedAmount}\\n`,\n '---\\n',\n '## Date of payment\\n',\n `${escapeMarkdown(dateOfPayment)}\\n`,\n '---\\n'\n )\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"v1.js","names":["addDays","format","dateFormat","config","getAnswer","escapeMarkdown","PaymentField","formatCurrency","formatPaymentDate","designerUrl","get","_context","items","model","submitResponse","formStatus","_formMetadata","files","result","formName","name","now","Date","formattedNow","fileExpiryDate","formattedExpiryDate","lines","push","isPreview","state","regularItems","filter","item","isPaymentItem","paymentItems","forEach","label","filename","fileId","repeaters","field","main","appendPaymentSection","join","length","paymentItem","paymentField","paymentState","getPaymentStateFromState","formattedAmount","amount","dateOfPayment","preAuth","createdAt","description"],"sources":["../../../../../../src/server/plugins/engine/outputFormatters/human/v1.ts"],"sourcesContent":["import {\n type FormMetadata,\n type SubmitResponsePayload\n} from '@defra/forms-model'\nimport { addDays, format as dateFormat } from 'date-fns'\n\nimport { config } from '~/src/config/index.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers/index.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/index.js'\nimport { type checkFormStatus } from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport { type FormContext } from '~/src/server/plugins/engine/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nconst designerUrl = config.get('designerUrl')\n\nexport function format(\n _context: FormContext,\n items: DetailItem[],\n model: FormModel,\n submitResponse: SubmitResponsePayload,\n formStatus: ReturnType<typeof checkFormStatus>,\n _formMetadata?: FormMetadata\n) {\n const { files } = submitResponse.result\n\n const formName = escapeMarkdown(model.name)\n\n /**\n * @todo Refactor this below but the code to\n * generate the question and answers works for now\n */\n const now = new Date()\n const formattedNow = `${dateFormat(now, 'h:mmaaa')} on ${dateFormat(now, 'd MMMM yyyy')}`\n\n const fileExpiryDate = addDays(now, 90)\n const formattedExpiryDate = `${dateFormat(fileExpiryDate, 'h:mmaaa')} on ${dateFormat(fileExpiryDate, 'eeee d MMMM yyyy')}`\n\n const lines: string[] = []\n\n lines.push(\n `^ For security reasons, the links in this email expire at ${escapeMarkdown(formattedExpiryDate)}\\n`\n )\n\n if (formStatus.isPreview) {\n lines.push(`This is a test of the ${formName} ${formStatus.state} form.\\n`)\n }\n\n lines.push(`${formName} form received at ${escapeMarkdown(formattedNow)}.\\n`)\n lines.push('---\\n')\n\n const regularItems = items.filter((item) => !isPaymentItem(item))\n const paymentItems = items.filter((item) => isPaymentItem(item))\n\n regularItems.forEach((item) => {\n const label = escapeMarkdown(item.label)\n\n lines.push(`## ${label}\\n`)\n\n if ('subItems' in item) {\n const filename = escapeMarkdown(`Download ${label} (CSV)`)\n const fileId = files.repeaters[item.name]\n\n lines.push(`[${filename}](${designerUrl}/file-download/${fileId})\\n`)\n } else {\n lines.push(\n getAnswer(item.field, item.state, {\n format: 'email'\n })\n )\n }\n\n lines.push('---\\n')\n })\n\n const filename = escapeMarkdown('Download main form (CSV)')\n lines.push(`[${filename}](${designerUrl}/file-download/${files.main})\\n`)\n\n appendPaymentSection(paymentItems, lines)\n\n return lines.join('\\n')\n}\n\n/**\n * Check if an item is a PaymentField\n */\nfunction isPaymentItem(item: DetailItem): boolean {\n if ('subItems' in item) {\n return false\n }\n return item.field instanceof PaymentField\n}\n\n/**\n * Appends the payment details section to the email lines if payment exists\n */\nfunction appendPaymentSection(paymentItems: DetailItem[], lines: string[]) {\n if (paymentItems.length === 0) {\n return\n }\n\n const paymentItem = paymentItems[0] as DetailItemField\n const paymentField = paymentItem.field as PaymentField\n const paymentState = paymentField.getPaymentStateFromState(paymentItem.state)\n\n if (!paymentState) {\n return\n }\n\n const formattedAmount = formatCurrency(paymentState.amount)\n const dateOfPayment = paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n\n lines.push(\n '---\\n',\n `# Your payment of ${formattedAmount} was successful\\n`,\n '## Payment for\\n',\n `${escapeMarkdown(paymentState.description)}\\n`,\n '---\\n',\n '## Total amount\\n',\n `${formattedAmount}\\n`,\n '---\\n',\n '## Date of payment\\n',\n `${escapeMarkdown(dateOfPayment)}\\n`,\n '---\\n'\n )\n}\n"],"mappings":"AAIA,SAASA,OAAO,EAAEC,MAAM,IAAIC,UAAU,QAAQ,UAAU;AAExD,SAASC,MAAM;AACf,SAASC,SAAS;AAClB,SAASC,cAAc;AACvB,SAASC,YAAY;AAQrB,SACEC,cAAc,EACdC,iBAAiB;AAGnB,MAAMC,WAAW,GAAGN,MAAM,CAACO,GAAG,CAAC,aAAa,CAAC;AAE7C,OAAO,SAAST,MAAMA,CACpBU,QAAqB,EACrBC,KAAmB,EACnBC,KAAgB,EAChBC,cAAqC,EACrCC,UAA8C,EAC9CC,aAA4B,EAC5B;EACA,MAAM;IAAEC;EAAM,CAAC,GAAGH,cAAc,CAACI,MAAM;EAEvC,MAAMC,QAAQ,GAAGd,cAAc,CAACQ,KAAK,CAACO,IAAI,CAAC;;EAE3C;AACF;AACA;AACA;EACE,MAAMC,GAAG,GAAG,IAAIC,IAAI,CAAC,CAAC;EACtB,MAAMC,YAAY,GAAG,GAAGrB,UAAU,CAACmB,GAAG,EAAE,SAAS,CAAC,OAAOnB,UAAU,CAACmB,GAAG,EAAE,aAAa,CAAC,EAAE;EAEzF,MAAMG,cAAc,GAAGxB,OAAO,CAACqB,GAAG,EAAE,EAAE,CAAC;EACvC,MAAMI,mBAAmB,GAAG,GAAGvB,UAAU,CAACsB,cAAc,EAAE,SAAS,CAAC,OAAOtB,UAAU,CAACsB,cAAc,EAAE,kBAAkB,CAAC,EAAE;EAE3H,MAAME,KAAe,GAAG,EAAE;EAE1BA,KAAK,CAACC,IAAI,CACR,6DAA6DtB,cAAc,CAACoB,mBAAmB,CAAC,IAClG,CAAC;EAED,IAAIV,UAAU,CAACa,SAAS,EAAE;IACxBF,KAAK,CAACC,IAAI,CAAC,yBAAyBR,QAAQ,IAAIJ,UAAU,CAACc,KAAK,UAAU,CAAC;EAC7E;EAEAH,KAAK,CAACC,IAAI,CAAC,GAAGR,QAAQ,qBAAqBd,cAAc,CAACkB,YAAY,CAAC,KAAK,CAAC;EAC7EG,KAAK,CAACC,IAAI,CAAC,OAAO,CAAC;EAEnB,MAAMG,YAAY,GAAGlB,KAAK,CAACmB,MAAM,CAAEC,IAAI,IAAK,CAACC,aAAa,CAACD,IAAI,CAAC,CAAC;EACjE,MAAME,YAAY,GAAGtB,KAAK,CAACmB,MAAM,CAAEC,IAAI,IAAKC,aAAa,CAACD,IAAI,CAAC,CAAC;EAEhEF,YAAY,CAACK,OAAO,CAAEH,IAAI,IAAK;IAC7B,MAAMI,KAAK,GAAG/B,cAAc,CAAC2B,IAAI,CAACI,KAAK,CAAC;IAExCV,KAAK,CAACC,IAAI,CAAC,MAAMS,KAAK,IAAI,CAAC;IAE3B,IAAI,UAAU,IAAIJ,IAAI,EAAE;MACtB,MAAMK,QAAQ,GAAGhC,cAAc,CAAC,YAAY+B,KAAK,QAAQ,CAAC;MAC1D,MAAME,MAAM,GAAGrB,KAAK,CAACsB,SAAS,CAACP,IAAI,CAACZ,IAAI,CAAC;MAEzCM,KAAK,CAACC,IAAI,CAAC,IAAIU,QAAQ,KAAK5B,WAAW,kBAAkB6B,MAAM,KAAK,CAAC;IACvE,CAAC,MAAM;MACLZ,KAAK,CAACC,IAAI,CACRvB,SAAS,CAAC4B,IAAI,CAACQ,KAAK,EAAER,IAAI,CAACH,KAAK,EAAE;QAChC5B,MAAM,EAAE;MACV,CAAC,CACH,CAAC;IACH;IAEAyB,KAAK,CAACC,IAAI,CAAC,OAAO,CAAC;EACrB,CAAC,CAAC;EAEF,MAAMU,QAAQ,GAAGhC,cAAc,CAAC,0BAA0B,CAAC;EAC3DqB,KAAK,CAACC,IAAI,CAAC,IAAIU,QAAQ,KAAK5B,WAAW,kBAAkBQ,KAAK,CAACwB,IAAI,KAAK,CAAC;EAEzEC,oBAAoB,CAACR,YAAY,EAAER,KAAK,CAAC;EAEzC,OAAOA,KAAK,CAACiB,IAAI,CAAC,IAAI,CAAC;AACzB;;AAEA;AACA;AACA;AACA,SAASV,aAAaA,CAACD,IAAgB,EAAW;EAChD,IAAI,UAAU,IAAIA,IAAI,EAAE;IACtB,OAAO,KAAK;EACd;EACA,OAAOA,IAAI,CAACQ,KAAK,YAAYlC,YAAY;AAC3C;;AAEA;AACA;AACA;AACA,SAASoC,oBAAoBA,CAACR,YAA0B,EAAER,KAAe,EAAE;EACzE,IAAIQ,YAAY,CAACU,MAAM,KAAK,CAAC,EAAE;IAC7B;EACF;EAEA,MAAMC,WAAW,GAAGX,YAAY,CAAC,CAAC,CAAoB;EACtD,MAAMY,YAAY,GAAGD,WAAW,CAACL,KAAqB;EACtD,MAAMO,YAAY,GAAGD,YAAY,CAACE,wBAAwB,CAACH,WAAW,CAAChB,KAAK,CAAC;EAE7E,IAAI,CAACkB,YAAY,EAAE;IACjB;EACF;EAEA,MAAME,eAAe,GAAG1C,cAAc,CAACwC,YAAY,CAACG,MAAM,CAAC;EAC3D,MAAMC,aAAa,GAAGJ,YAAY,CAACK,OAAO,EAAEC,SAAS,GACjD7C,iBAAiB,CAACuC,YAAY,CAACK,OAAO,CAACC,SAAS,CAAC,GACjD,EAAE;EAEN3B,KAAK,CAACC,IAAI,CACR,OAAO,EACP,qBAAqBsB,eAAe,mBAAmB,EACvD,kBAAkB,EAClB,GAAG5C,cAAc,CAAC0C,YAAY,CAACO,WAAW,CAAC,IAAI,EAC/C,OAAO,EACP,mBAAmB,EACnB,GAAGL,eAAe,IAAI,EACtB,OAAO,EACP,sBAAsB,EACtB,GAAG5C,cAAc,CAAC8C,aAAa,CAAC,IAAI,EACpC,OACF,CAAC;AACH","ignoreList":[]}
@@ -8,7 +8,7 @@ import { SummaryViewModel } from "../models/index.js";
8
8
  import { QuestionPageController } from "./QuestionPageController.js";
9
9
  import { InvalidComponentStateError, PaymentErrorTypes, PaymentPreAuthError, PaymentSubmissionError } from "./errors.js";
10
10
  import { buildMainRecords, buildRepeaterRecords } from "./helpers/submission.js";
11
- import { DEFAULT_PAYMENT_HELP_URL, formatPaymentAmount, formatPaymentDate } from "../../payment/helper.js";
11
+ import { DEFAULT_PAYMENT_HELP_URL, formatCurrency, formatPaymentDate } from "../../payment/helper.js";
12
12
  import { FormAction } from "../../../routes/types.js";
13
13
  export class SummaryPageController extends QuestionPageController {
14
14
  allowSaveAndExit = true;
@@ -67,7 +67,7 @@ export class SummaryPageController extends QuestionPageController {
67
67
  text: 'Total amount'
68
68
  },
69
69
  value: {
70
- text: formatPaymentAmount(paymentState.amount)
70
+ text: formatCurrency(paymentState.amount)
71
71
  }
72
72
  }, {
73
73
  key: {
@@ -1 +1 @@
1
- {"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","COMPONENT_STATE_ERROR","PAYMENT_EXPIRED_NOTIFICATION","ComponentCollection","PaymentField","checkEmailAddressForLiveFormSubmission","checkFormStatus","createError","getCacheService","SummaryViewModel","QuestionPageController","InvalidComponentStateError","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","buildMainRecords","buildRepeaterRecords","DEFAULT_PAYMENT_HELP_URL","formatPaymentAmount","formatPaymentDate","FormAction","SummaryPageController","allowSaveAndExit","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","state","paymentField","relevantPages","flatMap","fields","find","field","paymentState","getPaymentStateFromState","paymentDetails","buildPaymentDetails","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","shouldShowSaveAndExit","server","rows","key","text","value","description","amount","reference","preAuth","createdAt","push","title","summaryList","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","action","SaveAndExit","handleSaveAndExit","handleFormSubmit","params","cacheService","formsService","services","getFormMetadata","formMetadata","slug","notificationEmail","isPreview","submitForm","error","handleSubmissionError","setConfirmationState","confirmed","formId","referenceNumber","clearState","proceed","getStatusPath","handleInvalidComponentStateError","handlePaymentPreAuthError","handlePaymentSubmissionError","govukError","component","name","userMessage","yar","flash","resetComponentStates","getStateKeys","path","shouldResetState","errorType","PaymentExpired","redirectPath","undefined","helpUrl","helpLink","helpLinkHtml","postRouteOptions","ext","onPreHandler","method","continue","metadata","summaryViewModel","emailAddress","finaliseComponents","paymentWasCaptured","hasPaymentBeenCaptured","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","id","badRequest","outputService","submit","err","contact","online","url","capture","status","relevantFields","onSubmit","retrievalKey","sessionId","formSubmissionService","main","repeaters","map","href","filter","flat","paymentItems","getPaymentFieldItems","label","getDisplayStringFromState"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type FormMetadata,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions } from '@hapi/hapi'\n\nimport {\n COMPONENT_STATE_ERROR,\n PAYMENT_EXPIRED_NOTIFICATION\n} from '~/src/server/constants.js'\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n createError,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n InvalidComponentStateError,\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n buildMainRecords,\n buildRepeaterRecords\n} from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'\nimport {\n type FormConfirmationState,\n type FormContext,\n type FormContextRequest\n} from '~/src/server/plugins/engine/types.js'\nimport {\n DEFAULT_PAYMENT_HELP_URL,\n formatPaymentAmount,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n allowSaveAndExit = true\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors, state } = context\n\n const paymentField = context.relevantPages\n .flatMap((page) => page.collection.fields)\n .find((field): field is PaymentField => field instanceof PaymentField)\n\n if (paymentField) {\n const paymentState = paymentField.getPaymentStateFromState(state)\n if (paymentState) {\n viewModel.paymentState = paymentState\n viewModel.paymentDetails = this.buildPaymentDetails(\n paymentField,\n paymentState\n )\n }\n }\n\n const components = this.collection.getViewModel(payload, errors, query)\n\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server)\n viewModel.errors = errors\n\n return viewModel\n }\n\n private buildPaymentDetails(\n paymentField: PaymentField,\n paymentState: NonNullable<\n ReturnType<PaymentField['getPaymentStateFromState']>\n >\n ) {\n const rows = [\n {\n key: { text: 'Payment for' },\n value: { text: paymentState.description }\n },\n {\n key: { text: 'Total amount' },\n value: { text: formatPaymentAmount(paymentState.amount) }\n },\n {\n key: { text: 'Reference' },\n value: { text: paymentState.reference }\n }\n ]\n\n if (paymentState.preAuth?.createdAt) {\n rows.push({\n key: { text: 'Date of payment' },\n value: { text: formatPaymentDate(paymentState.preAuth.createdAt) }\n })\n }\n\n return {\n title: { text: 'Payment details' },\n summaryList: { rows }\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { action } = request.payload\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n return this.handleFormSubmit(request, context, h)\n }\n }\n\n async handleFormSubmit(\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) {\n const { model } = this\n const { params } = request\n\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n const formMetadata = await getFormMetadata(params.slug)\n const { notificationEmail } = formMetadata\n const { isPreview } = checkFormStatus(request.params)\n\n checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview)\n\n if (notificationEmail) {\n const viewModel = this.getSummaryViewModel(request, context)\n\n try {\n await submitForm(\n context,\n formMetadata,\n request,\n viewModel,\n model,\n notificationEmail,\n formMetadata\n )\n } catch (error) {\n return this.handleSubmissionError(error, request, h)\n }\n }\n\n await cacheService.setConfirmationState(request, {\n confirmed: true,\n formId: context.state.formId,\n referenceNumber: context.referenceNumber\n } as FormConfirmationState)\n\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n\n /**\n * Handles errors during form submission\n */\n private async handleSubmissionError(\n error: unknown,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n if (error instanceof InvalidComponentStateError) {\n return this.handleInvalidComponentStateError(error, request, h)\n }\n\n if (error instanceof PaymentPreAuthError) {\n return this.handlePaymentPreAuthError(error, request, h)\n }\n\n if (error instanceof PaymentSubmissionError) {\n return this.handlePaymentSubmissionError(error, request, h)\n }\n\n throw error\n }\n\n /**\n * Handles InvalidComponentStateError during submission\n */\n private async handleInvalidComponentStateError(\n error: InvalidComponentStateError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n const govukError = createError(error.component.name, error.userMessage)\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n return this.proceed(request, h, error.component.page?.path)\n }\n\n /**\n * Handles PaymentPreAuthError during submission\n */\n private async handlePaymentPreAuthError(\n error: PaymentPreAuthError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n if (error.shouldResetState) {\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n if (error.errorType === PaymentErrorTypes.PaymentExpired) {\n request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)\n return this.proceed(request, h, error.component.page?.path)\n }\n }\n\n const govukError = createError(error.component.name, error.userMessage)\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n const redirectPath = error.shouldResetState\n ? error.component.page?.path\n : undefined\n\n return this.proceed(request, h, redirectPath)\n }\n\n /**\n * Handles PaymentSubmissionError during submission\n */\n private handlePaymentSubmissionError(\n error: PaymentSubmissionError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL\n const helpLinkHtml = ` or you can <a href=\"${helpUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"govuk-link\">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`\n\n const govukError = createError(\n 'submission',\n `There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`\n )\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n return this.proceed(request, h)\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n}\n\nexport async function submitForm(\n context: FormContext,\n metadata: FormMetadata,\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n emailAddress: string,\n formMetadata: FormMetadata\n) {\n await finaliseComponents(request, metadata, context)\n\n const paymentWasCaptured = hasPaymentBeenCaptured(context)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n try {\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n await model.services.outputService.submit(\n context,\n request,\n model,\n emailAddress,\n items,\n submitResponse,\n formMetadata\n )\n } catch (err) {\n if (paymentWasCaptured) {\n throw new PaymentSubmissionError(\n context.referenceNumber,\n formMetadata.contact?.online?.url\n )\n }\n throw err\n }\n}\n\n/**\n * Checks if any payment component has been captured\n */\nfunction hasPaymentBeenCaptured(context: FormContext): boolean {\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n const paymentState = field.getPaymentStateFromState(context.state)\n if (paymentState?.capture?.status === 'success') {\n return true\n }\n }\n }\n }\n return false\n}\n\n/**\n * Finalises any components that need post-processing before form submission. Candidates usually involve\n * those that have external state.\n * Examples include:\n * - file uploads which are 'persisted' before submission\n * - payments which are 'captured' before submission\n */\nasync function finaliseComponents(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n) {\n const relevantFields = context.relevantPages.flatMap(\n (page) => page.collection.fields\n )\n\n for (const component of relevantFields) {\n /*\n Each component will throw InvalidComponent if its state is invalid, which is handled\n by handleFormSubmit\n */\n await component.onSubmit(request, metadata, context)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n main: buildMainRecords(items),\n repeaters: buildRepeaterRecords(items)\n }\n\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n const items = context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n\n const paymentItems = getPaymentFieldItems(context)\n\n return [...items, ...paymentItems]\n}\n\n/**\n * Gets DetailItems for PaymentField components\n * PaymentField is excluded from summaryDetails for UI but needs to be in submission data\n */\nfunction getPaymentFieldItems(context: FormContext): DetailItemField[] {\n const items: DetailItemField[] = []\n\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n items.push({\n name: field.name,\n page,\n title: field.title,\n label: field.label,\n field,\n state: context.state,\n href: page.href,\n value: field.getDisplayStringFromState(context.state)\n })\n }\n }\n }\n\n return items\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"SummaryPageController.js","names":["hasComponentsEvenIfNoNext","Boom","COMPONENT_STATE_ERROR","PAYMENT_EXPIRED_NOTIFICATION","ComponentCollection","PaymentField","checkEmailAddressForLiveFormSubmission","checkFormStatus","createError","getCacheService","SummaryViewModel","QuestionPageController","InvalidComponentStateError","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","buildMainRecords","buildRepeaterRecords","DEFAULT_PAYMENT_HELP_URL","formatCurrency","formatPaymentDate","FormAction","SummaryPageController","allowSaveAndExit","constructor","model","pageDef","viewName","collection","components","page","getSummaryViewModel","request","context","viewModel","query","payload","errors","state","paymentField","relevantPages","flatMap","fields","find","field","paymentState","getPaymentStateFromState","paymentDetails","buildPaymentDetails","getViewModel","backLink","getBackLink","feedbackLink","phaseTag","shouldShowSaveAndExit","server","rows","key","text","value","description","amount","reference","preAuth","createdAt","push","title","summaryList","makeGetRouteHandler","h","hasMissingNotificationEmail","view","makePostRouteHandler","action","SaveAndExit","handleSaveAndExit","handleFormSubmit","params","cacheService","formsService","services","getFormMetadata","formMetadata","slug","notificationEmail","isPreview","submitForm","error","handleSubmissionError","setConfirmationState","confirmed","formId","referenceNumber","clearState","proceed","getStatusPath","handleInvalidComponentStateError","handlePaymentPreAuthError","handlePaymentSubmissionError","govukError","component","name","userMessage","yar","flash","resetComponentStates","getStateKeys","path","shouldResetState","errorType","PaymentExpired","redirectPath","undefined","helpUrl","helpLink","helpLinkHtml","postRouteOptions","ext","onPreHandler","method","continue","metadata","summaryViewModel","emailAddress","finaliseComponents","paymentWasCaptured","hasPaymentBeenCaptured","formStatus","logTags","logger","info","items","getFormSubmissionData","details","submitResponse","submitData","id","badRequest","outputService","submit","err","contact","online","url","capture","status","relevantFields","onSubmit","retrievalKey","sessionId","formSubmissionService","main","repeaters","map","href","filter","flat","paymentItems","getPaymentFieldItems","label","getDisplayStringFromState"],"sources":["../../../../../src/server/plugins/engine/pageControllers/SummaryPageController.ts"],"sourcesContent":["import {\n hasComponentsEvenIfNoNext,\n type FormMetadata,\n type Page,\n type SubmitPayload\n} from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { type RouteOptions } from '@hapi/hapi'\n\nimport {\n COMPONENT_STATE_ERROR,\n PAYMENT_EXPIRED_NOTIFICATION\n} from '~/src/server/constants.js'\nimport { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport {\n checkEmailAddressForLiveFormSubmission,\n checkFormStatus,\n createError,\n getCacheService\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n SummaryViewModel,\n type FormModel\n} from '~/src/server/plugins/engine/models/index.js'\nimport {\n type Detail,\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport {\n InvalidComponentStateError,\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n buildMainRecords,\n buildRepeaterRecords\n} from '~/src/server/plugins/engine/pageControllers/helpers/submission.js'\nimport {\n type FormConfirmationState,\n type FormContext,\n type FormContextRequest\n} from '~/src/server/plugins/engine/types.js'\nimport {\n DEFAULT_PAYMENT_HELP_URL,\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormRequestPayloadRefs,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class SummaryPageController extends QuestionPageController {\n declare pageDef: Page\n allowSaveAndExit = true\n\n /**\n * The controller which is used when Page[\"controller\"] is defined as \"./pages/summary.js\"\n */\n\n constructor(model: FormModel, pageDef: Page) {\n super(model, pageDef)\n this.viewName = 'summary'\n\n // Components collection\n this.collection = new ComponentCollection(\n hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],\n { model, page: this }\n )\n }\n\n getSummaryViewModel(\n request: FormContextRequest,\n context: FormContext\n ): SummaryViewModel {\n const viewModel = new SummaryViewModel(request, this, context)\n\n const { query } = request\n const { payload, errors, state } = context\n\n const paymentField = context.relevantPages\n .flatMap((page) => page.collection.fields)\n .find((field): field is PaymentField => field instanceof PaymentField)\n\n if (paymentField) {\n const paymentState = paymentField.getPaymentStateFromState(state)\n if (paymentState) {\n viewModel.paymentState = paymentState\n viewModel.paymentDetails = this.buildPaymentDetails(\n paymentField,\n paymentState\n )\n }\n }\n\n const components = this.collection.getViewModel(payload, errors, query)\n\n viewModel.backLink = this.getBackLink(request, context)\n viewModel.feedbackLink = this.feedbackLink\n viewModel.phaseTag = this.phaseTag\n viewModel.components = components\n viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server)\n viewModel.errors = errors\n\n return viewModel\n }\n\n private buildPaymentDetails(\n paymentField: PaymentField,\n paymentState: NonNullable<\n ReturnType<PaymentField['getPaymentStateFromState']>\n >\n ) {\n const rows = [\n {\n key: { text: 'Payment for' },\n value: { text: paymentState.description }\n },\n {\n key: { text: 'Total amount' },\n value: { text: formatCurrency(paymentState.amount) }\n },\n {\n key: { text: 'Reference' },\n value: { text: paymentState.reference }\n }\n ]\n\n if (paymentState.preAuth?.createdAt) {\n rows.push({\n key: { text: 'Date of payment' },\n value: { text: formatPaymentDate(paymentState.preAuth.createdAt) }\n })\n }\n\n return {\n title: { text: 'Payment details' },\n summaryList: { rows }\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a GET request at `/{id}/{path*}`,\n */\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewName } = this\n\n const viewModel = this.getSummaryViewModel(request, context)\n\n viewModel.hasMissingNotificationEmail =\n await this.hasMissingNotificationEmail(request, context)\n\n return h.view(viewName, viewModel)\n }\n }\n\n /**\n * Returns an async function. This is called in plugin.ts when there is a POST request at `/{id}/{path*}`.\n * If a form is incomplete, a user will be redirected to the start page.\n */\n makePostRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { action } = request.payload\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n return this.handleFormSubmit(request, context, h)\n }\n }\n\n async handleFormSubmit(\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) {\n const { model } = this\n const { params } = request\n\n const cacheService = getCacheService(request.server)\n\n const { formsService } = this.model.services\n const { getFormMetadata } = formsService\n\n const formMetadata = await getFormMetadata(params.slug)\n const { notificationEmail } = formMetadata\n const { isPreview } = checkFormStatus(request.params)\n\n checkEmailAddressForLiveFormSubmission(notificationEmail, isPreview)\n\n if (notificationEmail) {\n const viewModel = this.getSummaryViewModel(request, context)\n\n try {\n await submitForm(\n context,\n formMetadata,\n request,\n viewModel,\n model,\n notificationEmail,\n formMetadata\n )\n } catch (error) {\n return this.handleSubmissionError(error, request, h)\n }\n }\n\n await cacheService.setConfirmationState(request, {\n confirmed: true,\n formId: context.state.formId,\n referenceNumber: context.referenceNumber\n } as FormConfirmationState)\n\n await cacheService.clearState(request)\n\n return this.proceed(request, h, this.getStatusPath())\n }\n\n /**\n * Handles errors during form submission\n */\n private async handleSubmissionError(\n error: unknown,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n if (error instanceof InvalidComponentStateError) {\n return this.handleInvalidComponentStateError(error, request, h)\n }\n\n if (error instanceof PaymentPreAuthError) {\n return this.handlePaymentPreAuthError(error, request, h)\n }\n\n if (error instanceof PaymentSubmissionError) {\n return this.handlePaymentSubmissionError(error, request, h)\n }\n\n throw error\n }\n\n /**\n * Handles InvalidComponentStateError during submission\n */\n private async handleInvalidComponentStateError(\n error: InvalidComponentStateError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n const govukError = createError(error.component.name, error.userMessage)\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n return this.proceed(request, h, error.component.page?.path)\n }\n\n /**\n * Handles PaymentPreAuthError during submission\n */\n private async handlePaymentPreAuthError(\n error: PaymentPreAuthError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const cacheService = getCacheService(request.server)\n\n if (error.shouldResetState) {\n await cacheService.resetComponentStates(request, error.getStateKeys())\n\n if (error.errorType === PaymentErrorTypes.PaymentExpired) {\n request.yar.flash(PAYMENT_EXPIRED_NOTIFICATION, true, true)\n return this.proceed(request, h, error.component.page?.path)\n }\n }\n\n const govukError = createError(error.component.name, error.userMessage)\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n const redirectPath = error.shouldResetState\n ? error.component.page?.path\n : undefined\n\n return this.proceed(request, h, redirectPath)\n }\n\n /**\n * Handles PaymentSubmissionError during submission\n */\n private handlePaymentSubmissionError(\n error: PaymentSubmissionError,\n request: FormRequestPayload,\n h: FormResponseToolkit\n ) {\n const helpUrl = error.helpLink ?? DEFAULT_PAYMENT_HELP_URL\n const helpLinkHtml = ` or you can <a href=\"${helpUrl}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"govuk-link\">contact us (opens in new tab)</a> and quote your reference number to arrange a refund`\n\n const govukError = createError(\n 'submission',\n `There was a problem and your form was not submitted. Try submitting the form again${helpLinkHtml}.`\n )\n\n request.yar.flash(COMPONENT_STATE_ERROR, govukError, true)\n\n return this.proceed(request, h)\n }\n\n get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {\n return {\n ext: {\n onPreHandler: {\n method(request, h) {\n return h.continue\n }\n }\n }\n }\n }\n}\n\nexport async function submitForm(\n context: FormContext,\n metadata: FormMetadata,\n request: FormRequestPayload,\n summaryViewModel: SummaryViewModel,\n model: FormModel,\n emailAddress: string,\n formMetadata: FormMetadata\n) {\n await finaliseComponents(request, metadata, context)\n\n const paymentWasCaptured = hasPaymentBeenCaptured(context)\n\n const formStatus = checkFormStatus(request.params)\n const logTags = ['submit', 'submissionApi']\n\n request.logger.info(logTags, 'Preparing email', formStatus)\n\n const items = getFormSubmissionData(\n summaryViewModel.context,\n summaryViewModel.details\n )\n\n try {\n request.logger.info(logTags, 'Submitting data')\n const submitResponse = await submitData(\n model,\n items,\n emailAddress,\n request.yar.id\n )\n\n if (submitResponse === undefined) {\n throw Boom.badRequest('Unexpected empty response from submit api')\n }\n\n await model.services.outputService.submit(\n context,\n request,\n model,\n emailAddress,\n items,\n submitResponse,\n formMetadata\n )\n } catch (err) {\n if (paymentWasCaptured) {\n throw new PaymentSubmissionError(\n context.referenceNumber,\n formMetadata.contact?.online?.url\n )\n }\n throw err\n }\n}\n\n/**\n * Checks if any payment component has been captured\n */\nfunction hasPaymentBeenCaptured(context: FormContext): boolean {\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n const paymentState = field.getPaymentStateFromState(context.state)\n if (paymentState?.capture?.status === 'success') {\n return true\n }\n }\n }\n }\n return false\n}\n\n/**\n * Finalises any components that need post-processing before form submission. Candidates usually involve\n * those that have external state.\n * Examples include:\n * - file uploads which are 'persisted' before submission\n * - payments which are 'captured' before submission\n */\nasync function finaliseComponents(\n request: FormRequestPayload,\n metadata: FormMetadata,\n context: FormContext\n) {\n const relevantFields = context.relevantPages.flatMap(\n (page) => page.collection.fields\n )\n\n for (const component of relevantFields) {\n /*\n Each component will throw InvalidComponent if its state is invalid, which is handled\n by handleFormSubmit\n */\n await component.onSubmit(request, metadata, context)\n }\n}\n\nfunction submitData(\n model: FormModel,\n items: DetailItem[],\n retrievalKey: string,\n sessionId: string\n) {\n const { formSubmissionService } = model.services\n const { submit } = formSubmissionService\n\n const payload: SubmitPayload = {\n sessionId,\n retrievalKey,\n main: buildMainRecords(items),\n repeaters: buildRepeaterRecords(items)\n }\n\n return submit(payload)\n}\n\nexport function getFormSubmissionData(context: FormContext, details: Detail[]) {\n const items = context.relevantPages\n .map(({ href }) =>\n details.flatMap(({ items }) =>\n items.filter(({ page }) => page.href === href)\n )\n )\n .flat()\n\n const paymentItems = getPaymentFieldItems(context)\n\n return [...items, ...paymentItems]\n}\n\n/**\n * Gets DetailItems for PaymentField components\n * PaymentField is excluded from summaryDetails for UI but needs to be in submission data\n */\nfunction getPaymentFieldItems(context: FormContext): DetailItemField[] {\n const items: DetailItemField[] = []\n\n for (const page of context.relevantPages) {\n for (const field of page.collection.fields) {\n if (field instanceof PaymentField) {\n items.push({\n name: field.name,\n page,\n title: field.title,\n label: field.label,\n field,\n state: context.state,\n href: page.href,\n value: field.getDisplayStringFromState(context.state)\n })\n }\n }\n }\n\n return items\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1,6 +1,6 @@
1
1
  import { PaymentField } from "../../components/PaymentField.js";
2
2
  import { getAnswer } from "../../components/helpers/components.js";
3
- import { formatPaymentAmount, formatPaymentDate } from "../../../payment/helper.js";
3
+ import { formatCurrency, formatPaymentDate } from "../../../payment/helper.js";
4
4
  /**
5
5
  * Builds the main submission records from field items.
6
6
  * Regular fields are converted to single records, while PaymentField
@@ -46,7 +46,7 @@ export function buildPaymentRecords(item) {
46
46
  }, {
47
47
  name: `${item.name}_paymentAmount`,
48
48
  title: 'Payment amount',
49
- value: formatPaymentAmount(paymentState.amount)
49
+ value: formatCurrency(paymentState.amount)
50
50
  }, {
51
51
  name: `${item.name}_paymentReference`,
52
52
  title: 'Payment reference',
@@ -1 +1 @@
1
- {"version":3,"file":"submission.js","names":["PaymentField","getAnswer","formatPaymentAmount","formatPaymentDate","buildMainRecords","items","fieldItems","filter","item","records","field","push","buildPaymentRecords","name","title","label","value","state","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","buildRepeaterRecords","map","subItems","detailItems","subItem"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatPaymentAmount,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(items: DetailItem[]): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item))\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatPaymentAmount(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[]\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n}\n"],"mappings":"AAEA,SAASA,YAAY;AACrB,SAASC,SAAS;AAKlB,SACEC,mBAAmB,EACnBC,iBAAiB;AASnB;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,KAAmB,EAAkB;EACpE,MAAMC,UAAU,GAAGD,KAAK,CAACE,MAAM,CAC5BC,IAAI,IAA8B,OAAO,IAAIA,IAChD,CAAC;EAED,MAAMC,OAAuB,GAAG,EAAE;EAElC,KAAK,MAAMD,IAAI,IAAIF,UAAU,EAAE;IAC7B,IAAIE,IAAI,CAACE,KAAK,YAAYV,YAAY,EAAE;MACtCS,OAAO,CAACE,IAAI,CAAC,GAAGC,mBAAmB,CAACJ,IAAI,CAAC,CAAC;IAC5C,CAAC,MAAM;MACLC,OAAO,CAACE,IAAI,CAAC;QACXE,IAAI,EAAEL,IAAI,CAACK,IAAI;QACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;QACjBC,KAAK,EAAEf,SAAS,CAACO,IAAI,CAACE,KAAK,EAAEF,IAAI,CAACS,KAAK,EAAE;UAAEC,MAAM,EAAE;QAAO,CAAC;MAC7D,CAAC,CAAC;IACJ;EACF;EAEA,OAAOT,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,mBAAmBA,CAACJ,IAAqB,EAAkB;EACzE,MAAMW,YAAY,GAAIX,IAAI,CAACE,KAAK,CAAkBU,wBAAwB,CACxEZ,IAAI,CAACS,KACP,CAAC;EAED,IAAI,CAACE,YAAY,EAAE;IACjB,OAAO,EAAE;EACX;EAEA,OAAO,CACL;IACEN,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,qBAAqB;IACvCC,KAAK,EAAE,qBAAqB;IAC5BE,KAAK,EAAEG,YAAY,CAACE;EACtB,CAAC,EACD;IACER,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,gBAAgB;IAClCC,KAAK,EAAE,gBAAgB;IACvBE,KAAK,EAAEd,mBAAmB,CAACiB,YAAY,CAACG,MAAM;EAChD,CAAC,EACD;IACET,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,mBAAmB;IACrCC,KAAK,EAAE,mBAAmB;IAC1BE,KAAK,EAAEG,YAAY,CAACI;EACtB,CAAC,EACD;IACEV,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,cAAc;IAChCC,KAAK,EAAE,cAAc;IACrBE,KAAK,EAAEG,YAAY,CAACK,OAAO,EAAEC,SAAS,GAClCtB,iBAAiB,CAACgB,YAAY,CAACK,OAAO,CAACC,SAAS,CAAC,GACjD;EACN,CAAC,CACF;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAClCrB,KAAmB,EACS;EAC5B,OAAOA,KAAK,CACTE,MAAM,CAAEC,IAAI,IAAK,UAAU,IAAIA,IAAI,CAAC,CACpCmB,GAAG,CAAEnB,IAAI,KAAM;IACdK,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;IACjBC,KAAK,EAAER,IAAI,CAACoB,QAAQ,CAACD,GAAG,CAAEE,WAAW,IACnCA,WAAW,CAACF,GAAG,CAAEG,OAAO,KAAM;MAC5BjB,IAAI,EAAEiB,OAAO,CAACjB,IAAI;MAClBC,KAAK,EAAEgB,OAAO,CAACf,KAAK;MACpBC,KAAK,EAAEf,SAAS,CAAC6B,OAAO,CAACpB,KAAK,EAAEoB,OAAO,CAACb,KAAK,EAAE;QAAEC,MAAM,EAAE;MAAO,CAAC;IACnE,CAAC,CAAC,CACJ;EACF,CAAC,CAAC,CAAC;AACP","ignoreList":[]}
1
+ {"version":3,"file":"submission.js","names":["PaymentField","getAnswer","formatCurrency","formatPaymentDate","buildMainRecords","items","fieldItems","filter","item","records","field","push","buildPaymentRecords","name","title","label","value","state","format","paymentState","getPaymentStateFromState","description","amount","reference","preAuth","createdAt","buildRepeaterRecords","map","subItems","detailItems","subItem"],"sources":["../../../../../../src/server/plugins/engine/pageControllers/helpers/submission.ts"],"sourcesContent":["import { type SubmitPayload } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type DetailItem,\n type DetailItemField\n} from '~/src/server/plugins/engine/models/types.js'\nimport {\n formatCurrency,\n formatPaymentDate\n} from '~/src/server/plugins/payment/helper.js'\n\nexport interface SubmitRecord {\n name: string\n title: string\n value: string\n}\n\n/**\n * Builds the main submission records from field items.\n * Regular fields are converted to single records, while PaymentField\n * components are expanded into four separate records.\n */\nexport function buildMainRecords(items: DetailItem[]): SubmitRecord[] {\n const fieldItems = items.filter(\n (item): item is DetailItemField => 'field' in item\n )\n\n const records: SubmitRecord[] = []\n\n for (const item of fieldItems) {\n if (item.field instanceof PaymentField) {\n records.push(...buildPaymentRecords(item))\n } else {\n records.push({\n name: item.name,\n title: item.label,\n value: getAnswer(item.field, item.state, { format: 'data' })\n })\n }\n }\n\n return records\n}\n\n/**\n * Expands a PaymentField into four submission records:\n * - Payment description\n * - Payment amount (formatted with currency symbol)\n * - Payment reference\n * - Payment date (formatted date/time)\n *\n * Returns an empty array if no payment state exists.\n */\nexport function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {\n const paymentState = (item.field as PaymentField).getPaymentStateFromState(\n item.state\n )\n\n if (!paymentState) {\n return []\n }\n\n return [\n {\n name: `${item.name}_paymentDescription`,\n title: 'Payment description',\n value: paymentState.description\n },\n {\n name: `${item.name}_paymentAmount`,\n title: 'Payment amount',\n value: formatCurrency(paymentState.amount)\n },\n {\n name: `${item.name}_paymentReference`,\n title: 'Payment reference',\n value: paymentState.reference\n },\n {\n name: `${item.name}_paymentDate`,\n title: 'Payment date',\n value: paymentState.preAuth?.createdAt\n ? formatPaymentDate(paymentState.preAuth.createdAt)\n : ''\n }\n ]\n}\n\n/**\n * Builds the repeater submission records from repeater items.\n */\nexport function buildRepeaterRecords(\n items: DetailItem[]\n): SubmitPayload['repeaters'] {\n return items\n .filter((item) => 'subItems' in item)\n .map((item) => ({\n name: item.name,\n title: item.label,\n value: item.subItems.map((detailItems) =>\n detailItems.map((subItem) => ({\n name: subItem.name,\n title: subItem.label,\n value: getAnswer(subItem.field, subItem.state, { format: 'data' })\n }))\n )\n }))\n}\n"],"mappings":"AAEA,SAASA,YAAY;AACrB,SAASC,SAAS;AAKlB,SACEC,cAAc,EACdC,iBAAiB;AASnB;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,KAAmB,EAAkB;EACpE,MAAMC,UAAU,GAAGD,KAAK,CAACE,MAAM,CAC5BC,IAAI,IAA8B,OAAO,IAAIA,IAChD,CAAC;EAED,MAAMC,OAAuB,GAAG,EAAE;EAElC,KAAK,MAAMD,IAAI,IAAIF,UAAU,EAAE;IAC7B,IAAIE,IAAI,CAACE,KAAK,YAAYV,YAAY,EAAE;MACtCS,OAAO,CAACE,IAAI,CAAC,GAAGC,mBAAmB,CAACJ,IAAI,CAAC,CAAC;IAC5C,CAAC,MAAM;MACLC,OAAO,CAACE,IAAI,CAAC;QACXE,IAAI,EAAEL,IAAI,CAACK,IAAI;QACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;QACjBC,KAAK,EAAEf,SAAS,CAACO,IAAI,CAACE,KAAK,EAAEF,IAAI,CAACS,KAAK,EAAE;UAAEC,MAAM,EAAE;QAAO,CAAC;MAC7D,CAAC,CAAC;IACJ;EACF;EAEA,OAAOT,OAAO;AAChB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,mBAAmBA,CAACJ,IAAqB,EAAkB;EACzE,MAAMW,YAAY,GAAIX,IAAI,CAACE,KAAK,CAAkBU,wBAAwB,CACxEZ,IAAI,CAACS,KACP,CAAC;EAED,IAAI,CAACE,YAAY,EAAE;IACjB,OAAO,EAAE;EACX;EAEA,OAAO,CACL;IACEN,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,qBAAqB;IACvCC,KAAK,EAAE,qBAAqB;IAC5BE,KAAK,EAAEG,YAAY,CAACE;EACtB,CAAC,EACD;IACER,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,gBAAgB;IAClCC,KAAK,EAAE,gBAAgB;IACvBE,KAAK,EAAEd,cAAc,CAACiB,YAAY,CAACG,MAAM;EAC3C,CAAC,EACD;IACET,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,mBAAmB;IACrCC,KAAK,EAAE,mBAAmB;IAC1BE,KAAK,EAAEG,YAAY,CAACI;EACtB,CAAC,EACD;IACEV,IAAI,EAAE,GAAGL,IAAI,CAACK,IAAI,cAAc;IAChCC,KAAK,EAAE,cAAc;IACrBE,KAAK,EAAEG,YAAY,CAACK,OAAO,EAAEC,SAAS,GAClCtB,iBAAiB,CAACgB,YAAY,CAACK,OAAO,CAACC,SAAS,CAAC,GACjD;EACN,CAAC,CACF;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASC,oBAAoBA,CAClCrB,KAAmB,EACS;EAC5B,OAAOA,KAAK,CACTE,MAAM,CAAEC,IAAI,IAAK,UAAU,IAAIA,IAAI,CAAC,CACpCmB,GAAG,CAAEnB,IAAI,KAAM;IACdK,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,KAAK,EAAEN,IAAI,CAACO,KAAK;IACjBC,KAAK,EAAER,IAAI,CAACoB,QAAQ,CAACD,GAAG,CAAEE,WAAW,IACnCA,WAAW,CAACF,GAAG,CAAEG,OAAO,KAAM;MAC5BjB,IAAI,EAAEiB,OAAO,CAACjB,IAAI;MAClBC,KAAK,EAAEgB,OAAO,CAACf,KAAK;MACpBC,KAAK,EAAEf,SAAS,CAAC6B,OAAO,CAACpB,KAAK,EAAEoB,OAAO,CAACb,KAAK,EAAE;QAAEC,MAAM,EAAE;MAAO,CAAC;IACnE,CAAC,CAAC,CACJ;EACF,CAAC,CAAC,CAAC;AACP","ignoreList":[]}
@@ -28,7 +28,7 @@
28
28
  <p class="govuk-body">You can submit the form after you have added your payment details.</p>
29
29
 
30
30
  <p class="govuk-body govuk-!-margin-bottom-1">Total amount:</p>
31
- <p class="govuk-heading-l govuk-!-margin-bottom-4 govuk-!-padding-top-0"{{ amount }}</p>
31
+ <p class="govuk-heading-l govuk-!-margin-bottom-4 govuk-!-padding-top-0">{{ amount }}</p>
32
32
 
33
33
  {{ govukButton({
34
34
  text: "Add payment details",
@@ -21,10 +21,12 @@ export function createPaymentService(isLivePayment: boolean, formId: string): Pa
21
21
  */
22
22
  export function formatPaymentDate(isoString: string): string;
23
23
  /**
24
- * Formats a payment amount with two decimal places
24
+ * Formats a currency amount with thousand separators and two decimal places
25
25
  * @param {number} amount - amount in pounds
26
- * @returns {string} Formatted amount (e.g., "£10.00")
26
+ * @param {'en-GB'} [locale] - locale for formatting
27
+ * @param {'GBP'} [currency] - currency code
28
+ * @returns {string} Formatted amount (e.g., "£1,234.56")
27
29
  */
28
- export function formatPaymentAmount(amount: number): string;
30
+ export function formatCurrency(amount: number, locale?: "en-GB", currency?: "GBP"): string;
29
31
  export const DEFAULT_PAYMENT_HELP_URL: "https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs";
30
32
  import { PaymentService } from '~/src/server/plugins/payment/service.js';
@@ -39,11 +39,17 @@ export function formatPaymentDate(isoString) {
39
39
  }
40
40
 
41
41
  /**
42
- * Formats a payment amount with two decimal places
42
+ * Formats a currency amount with thousand separators and two decimal places
43
43
  * @param {number} amount - amount in pounds
44
- * @returns {string} Formatted amount (e.g., "£10.00")
44
+ * @param {'en-GB'} [locale] - locale for formatting
45
+ * @param {'GBP'} [currency] - currency code
46
+ * @returns {string} Formatted amount (e.g., "£1,234.56")
45
47
  */
46
- export function formatPaymentAmount(amount) {
47
- return `£${amount.toFixed(2)}`;
48
+ export function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') {
49
+ const formatter = new Intl.NumberFormat(locale, {
50
+ style: 'currency',
51
+ currency
52
+ });
53
+ return formatter.format(amount);
48
54
  }
49
55
  //# sourceMappingURL=helper.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helper.js","names":["format","PaymentService","DEFAULT_PAYMENT_HELP_URL","getPaymentApiKey","isLivePayment","formId","apiKeyValue","process","env","Error","createPaymentService","apiKey","formatPaymentDate","isoString","Date","formatPaymentAmount","amount","toFixed"],"sources":["../../../../src/server/plugins/payment/helper.js"],"sourcesContent":["import { format } from 'date-fns'\n\nimport { PaymentService } from '~/src/server/plugins/payment/service.js'\n\nexport const DEFAULT_PAYMENT_HELP_URL =\n 'https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs'\n\n/**\n * Determine which payment API key value to use.\n * If a draft preview form or a live preview form, read the TEST API key value specific to that form.\n * If a live (non-preview) form, read the LIVE API key value specific to that form.\n * @param {boolean} isLivePayment - true if this is a live payment (as opposed to a test one)\n * @param {string} formId - id of the form\n * @returns {string}\n */\nexport function getPaymentApiKey(isLivePayment, formId) {\n const apiKeyValue = isLivePayment\n ? process.env[`PAYMENT_PROVIDER_API_KEY_LIVE_${formId}`]\n : process.env[`PAYMENT_PROVIDER_API_KEY_TEST_${formId}`]\n\n if (!apiKeyValue) {\n throw new Error(\n `[payment] Missing payment api key for ${isLivePayment ? 'live' : 'test'} form id ${formId}`\n )\n }\n return apiKeyValue\n}\n\n/**\n * Creates a PaymentService instance with the appropriate API key\n * @param {boolean} isLivePayment - true if this is a live payment\n * @param {string} formId - id of the form\n * @returns {PaymentService}\n */\nexport function createPaymentService(isLivePayment, formId) {\n const apiKey = getPaymentApiKey(isLivePayment, formId)\n return new PaymentService(apiKey)\n}\n\n/**\n * Formats a payment date for display\n * @param {string} isoString - ISO date string\n * @returns {string} Formatted date string (e.g., \"26 January 2026 5:01pm\")\n */\nexport function formatPaymentDate(isoString) {\n return format(new Date(isoString), 'd MMMM yyyy h:mmaaa')\n}\n\n/**\n * Formats a payment amount with two decimal places\n * @param {number} amount - amount in pounds\n * @returns {string} Formatted amount (e.g., \"£10.00\")\n */\nexport function formatPaymentAmount(amount) {\n return `£${amount.toFixed(2)}`\n}\n"],"mappings":"AAAA,SAASA,MAAM,QAAQ,UAAU;AAEjC,SAASC,cAAc;AAEvB,OAAO,MAAMC,wBAAwB,GACnC,2FAA2F;;AAE7F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,aAAa,EAAEC,MAAM,EAAE;EACtD,MAAMC,WAAW,GAAGF,aAAa,GAC7BG,OAAO,CAACC,GAAG,CAAC,iCAAiCH,MAAM,EAAE,CAAC,GACtDE,OAAO,CAACC,GAAG,CAAC,iCAAiCH,MAAM,EAAE,CAAC;EAE1D,IAAI,CAACC,WAAW,EAAE;IAChB,MAAM,IAAIG,KAAK,CACb,yCAAyCL,aAAa,GAAG,MAAM,GAAG,MAAM,YAAYC,MAAM,EAC5F,CAAC;EACH;EACA,OAAOC,WAAW;AACpB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,oBAAoBA,CAACN,aAAa,EAAEC,MAAM,EAAE;EAC1D,MAAMM,MAAM,GAAGR,gBAAgB,CAACC,aAAa,EAAEC,MAAM,CAAC;EACtD,OAAO,IAAIJ,cAAc,CAACU,MAAM,CAAC;AACnC;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,SAAS,EAAE;EAC3C,OAAOb,MAAM,CAAC,IAAIc,IAAI,CAACD,SAAS,CAAC,EAAE,qBAAqB,CAAC;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,mBAAmBA,CAACC,MAAM,EAAE;EAC1C,OAAO,IAAIA,MAAM,CAACC,OAAO,CAAC,CAAC,CAAC,EAAE;AAChC","ignoreList":[]}
1
+ {"version":3,"file":"helper.js","names":["format","PaymentService","DEFAULT_PAYMENT_HELP_URL","getPaymentApiKey","isLivePayment","formId","apiKeyValue","process","env","Error","createPaymentService","apiKey","formatPaymentDate","isoString","Date","formatCurrency","amount","locale","currency","formatter","Intl","NumberFormat","style"],"sources":["../../../../src/server/plugins/payment/helper.js"],"sourcesContent":["import { format } from 'date-fns'\n\nimport { PaymentService } from '~/src/server/plugins/payment/service.js'\n\nexport const DEFAULT_PAYMENT_HELP_URL =\n 'https://www.gov.uk/government/organisations/department-for-environment-food-rural-affairs'\n\n/**\n * Determine which payment API key value to use.\n * If a draft preview form or a live preview form, read the TEST API key value specific to that form.\n * If a live (non-preview) form, read the LIVE API key value specific to that form.\n * @param {boolean} isLivePayment - true if this is a live payment (as opposed to a test one)\n * @param {string} formId - id of the form\n * @returns {string}\n */\nexport function getPaymentApiKey(isLivePayment, formId) {\n const apiKeyValue = isLivePayment\n ? process.env[`PAYMENT_PROVIDER_API_KEY_LIVE_${formId}`]\n : process.env[`PAYMENT_PROVIDER_API_KEY_TEST_${formId}`]\n\n if (!apiKeyValue) {\n throw new Error(\n `[payment] Missing payment api key for ${isLivePayment ? 'live' : 'test'} form id ${formId}`\n )\n }\n return apiKeyValue\n}\n\n/**\n * Creates a PaymentService instance with the appropriate API key\n * @param {boolean} isLivePayment - true if this is a live payment\n * @param {string} formId - id of the form\n * @returns {PaymentService}\n */\nexport function createPaymentService(isLivePayment, formId) {\n const apiKey = getPaymentApiKey(isLivePayment, formId)\n return new PaymentService(apiKey)\n}\n\n/**\n * Formats a payment date for display\n * @param {string} isoString - ISO date string\n * @returns {string} Formatted date string (e.g., \"26 January 2026 5:01pm\")\n */\nexport function formatPaymentDate(isoString) {\n return format(new Date(isoString), 'd MMMM yyyy h:mmaaa')\n}\n\n/**\n * Formats a currency amount with thousand separators and two decimal places\n * @param {number} amount - amount in pounds\n * @param {'en-GB'} [locale] - locale for formatting\n * @param {'GBP'} [currency] - currency code\n * @returns {string} Formatted amount (e.g., \"£1,234.56\")\n */\nexport function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') {\n const formatter = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency\n })\n\n return formatter.format(amount)\n}\n"],"mappings":"AAAA,SAASA,MAAM,QAAQ,UAAU;AAEjC,SAASC,cAAc;AAEvB,OAAO,MAAMC,wBAAwB,GACnC,2FAA2F;;AAE7F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,aAAa,EAAEC,MAAM,EAAE;EACtD,MAAMC,WAAW,GAAGF,aAAa,GAC7BG,OAAO,CAACC,GAAG,CAAC,iCAAiCH,MAAM,EAAE,CAAC,GACtDE,OAAO,CAACC,GAAG,CAAC,iCAAiCH,MAAM,EAAE,CAAC;EAE1D,IAAI,CAACC,WAAW,EAAE;IAChB,MAAM,IAAIG,KAAK,CACb,yCAAyCL,aAAa,GAAG,MAAM,GAAG,MAAM,YAAYC,MAAM,EAC5F,CAAC;EACH;EACA,OAAOC,WAAW;AACpB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASI,oBAAoBA,CAACN,aAAa,EAAEC,MAAM,EAAE;EAC1D,MAAMM,MAAM,GAAGR,gBAAgB,CAACC,aAAa,EAAEC,MAAM,CAAC;EACtD,OAAO,IAAIJ,cAAc,CAACU,MAAM,CAAC;AACnC;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,SAAS,EAAE;EAC3C,OAAOb,MAAM,CAAC,IAAIc,IAAI,CAACD,SAAS,CAAC,EAAE,qBAAqB,CAAC;AAC3D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,cAAcA,CAACC,MAAM,EAAEC,MAAM,GAAG,OAAO,EAAEC,QAAQ,GAAG,KAAK,EAAE;EACzE,MAAMC,SAAS,GAAG,IAAIC,IAAI,CAACC,YAAY,CAACJ,MAAM,EAAE;IAC9CK,KAAK,EAAE,UAAU;IACjBJ;EACF,CAAC,CAAC;EAEF,OAAOC,SAAS,CAACnB,MAAM,CAACgB,MAAM,CAAC;AACjC","ignoreList":[]}
@@ -1,5 +1,5 @@
1
1
  import { config } from "../../../config/index.js";
2
- import { formatPaymentAmount, formatPaymentDate, getPaymentApiKey } from "./helper.js";
2
+ import { formatCurrency, formatPaymentDate, getPaymentApiKey } from "./helper.js";
3
3
  describe('getPaymentApiKey', () => {
4
4
  config.set('paymentProviderApiKeyTest', 'TEST-API-KEY');
5
5
  const formId = 'form-id';
@@ -26,12 +26,18 @@ describe('formatPaymentDate', () => {
26
26
  expect(result).toBe('10 November 2025 5:01pm');
27
27
  });
28
28
  });
29
- describe('formatPaymentAmount', () => {
30
- it('should format whole number with two decimal places', () => {
31
- expect(formatPaymentAmount(10)).toBe('£10.00');
29
+ describe('formatCurrency', () => {
30
+ it('should format whole number with currency symbol', () => {
31
+ expect(formatCurrency(10)).toBe('£10.00');
32
32
  });
33
- it('should format decimal amount', () => {
34
- expect(formatPaymentAmount(99.5)).toBe('£99.50');
33
+ it('should format decimal amount with currency symbol', () => {
34
+ expect(formatCurrency(99.5)).toBe('£99.50');
35
+ });
36
+ it('should format large amounts with thousand separators', () => {
37
+ expect(formatCurrency(1234.56)).toBe('£1,234.56');
38
+ });
39
+ it('should format very large amounts with thousand separators', () => {
40
+ expect(formatCurrency(20000)).toBe('£20,000.00');
35
41
  });
36
42
  });
37
43
  //# sourceMappingURL=helper.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"helper.test.js","names":["config","formatPaymentAmount","formatPaymentDate","getPaymentApiKey","describe","set","formId","process","env","it","apiKey","expect","toBe","toThrow","result"],"sources":["../../../../src/server/plugins/payment/helper.test.js"],"sourcesContent":["import { config } from '~/src/config/index.js'\nimport {\n formatPaymentAmount,\n formatPaymentDate,\n getPaymentApiKey\n} from '~/src/server/plugins/payment/helper.js'\n\ndescribe('getPaymentApiKey', () => {\n config.set('paymentProviderApiKeyTest', 'TEST-API-KEY')\n const formId = 'form-id'\n process.env['PAYMENT_PROVIDER_API_KEY_LIVE_form-id'] = 'LIVE-API-KEY'\n process.env['PAYMENT_PROVIDER_API_KEY_TEST_form-id'] = 'TEST-API-KEY'\n\n it('should read test key when non-live form', () => {\n const apiKey = getPaymentApiKey(false, formId)\n expect(apiKey).toBe('TEST-API-KEY')\n })\n\n it('should read live key when live form', () => {\n const apiKey = getPaymentApiKey(true, formId)\n expect(apiKey).toBe('LIVE-API-KEY')\n })\n\n it('should throw if TEST key is missing', () => {\n expect(() => getPaymentApiKey(false, 'form-id-missing')).toThrow(\n 'Missing payment api key for test form id form-id-missing'\n )\n })\n\n it('should throw if LIVE key is missing', () => {\n expect(() => getPaymentApiKey(true, 'form-id-missing')).toThrow(\n 'Missing payment api key for live form id form-id-missing'\n )\n })\n})\n\ndescribe('formatPaymentDate', () => {\n it('should format ISO date string to en-GB format', () => {\n const result = formatPaymentDate('2025-11-10T17:01:29.000Z')\n expect(result).toBe('10 November 2025 5:01pm')\n })\n})\n\ndescribe('formatPaymentAmount', () => {\n it('should format whole number with two decimal places', () => {\n expect(formatPaymentAmount(10)).toBe('£10.00')\n })\n\n it('should format decimal amount', () => {\n expect(formatPaymentAmount(99.5)).toBe('£99.50')\n })\n})\n"],"mappings":"AAAA,SAASA,MAAM;AACf,SACEC,mBAAmB,EACnBC,iBAAiB,EACjBC,gBAAgB;AAGlBC,QAAQ,CAAC,kBAAkB,EAAE,MAAM;EACjCJ,MAAM,CAACK,GAAG,CAAC,2BAA2B,EAAE,cAAc,CAAC;EACvD,MAAMC,MAAM,GAAG,SAAS;EACxBC,OAAO,CAACC,GAAG,CAAC,uCAAuC,CAAC,GAAG,cAAc;EACrED,OAAO,CAACC,GAAG,CAAC,uCAAuC,CAAC,GAAG,cAAc;EAErEC,EAAE,CAAC,yCAAyC,EAAE,MAAM;IAClD,MAAMC,MAAM,GAAGP,gBAAgB,CAAC,KAAK,EAAEG,MAAM,CAAC;IAC9CK,MAAM,CAACD,MAAM,CAAC,CAACE,IAAI,CAAC,cAAc,CAAC;EACrC,CAAC,CAAC;EAEFH,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9C,MAAMC,MAAM,GAAGP,gBAAgB,CAAC,IAAI,EAAEG,MAAM,CAAC;IAC7CK,MAAM,CAACD,MAAM,CAAC,CAACE,IAAI,CAAC,cAAc,CAAC;EACrC,CAAC,CAAC;EAEFH,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9CE,MAAM,CAAC,MAAMR,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAACU,OAAO,CAC9D,0DACF,CAAC;EACH,CAAC,CAAC;EAEFJ,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9CE,MAAM,CAAC,MAAMR,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAACU,OAAO,CAC7D,0DACF,CAAC;EACH,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFT,QAAQ,CAAC,mBAAmB,EAAE,MAAM;EAClCK,EAAE,CAAC,+CAA+C,EAAE,MAAM;IACxD,MAAMK,MAAM,GAAGZ,iBAAiB,CAAC,0BAA0B,CAAC;IAC5DS,MAAM,CAACG,MAAM,CAAC,CAACF,IAAI,CAAC,yBAAyB,CAAC;EAChD,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFR,QAAQ,CAAC,qBAAqB,EAAE,MAAM;EACpCK,EAAE,CAAC,oDAAoD,EAAE,MAAM;IAC7DE,MAAM,CAACV,mBAAmB,CAAC,EAAE,CAAC,CAAC,CAACW,IAAI,CAAC,QAAQ,CAAC;EAChD,CAAC,CAAC;EAEFH,EAAE,CAAC,8BAA8B,EAAE,MAAM;IACvCE,MAAM,CAACV,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAACW,IAAI,CAAC,QAAQ,CAAC;EAClD,CAAC,CAAC;AACJ,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"helper.test.js","names":["config","formatCurrency","formatPaymentDate","getPaymentApiKey","describe","set","formId","process","env","it","apiKey","expect","toBe","toThrow","result"],"sources":["../../../../src/server/plugins/payment/helper.test.js"],"sourcesContent":["import { config } from '~/src/config/index.js'\nimport {\n formatCurrency,\n formatPaymentDate,\n getPaymentApiKey\n} from '~/src/server/plugins/payment/helper.js'\n\ndescribe('getPaymentApiKey', () => {\n config.set('paymentProviderApiKeyTest', 'TEST-API-KEY')\n const formId = 'form-id'\n process.env['PAYMENT_PROVIDER_API_KEY_LIVE_form-id'] = 'LIVE-API-KEY'\n process.env['PAYMENT_PROVIDER_API_KEY_TEST_form-id'] = 'TEST-API-KEY'\n\n it('should read test key when non-live form', () => {\n const apiKey = getPaymentApiKey(false, formId)\n expect(apiKey).toBe('TEST-API-KEY')\n })\n\n it('should read live key when live form', () => {\n const apiKey = getPaymentApiKey(true, formId)\n expect(apiKey).toBe('LIVE-API-KEY')\n })\n\n it('should throw if TEST key is missing', () => {\n expect(() => getPaymentApiKey(false, 'form-id-missing')).toThrow(\n 'Missing payment api key for test form id form-id-missing'\n )\n })\n\n it('should throw if LIVE key is missing', () => {\n expect(() => getPaymentApiKey(true, 'form-id-missing')).toThrow(\n 'Missing payment api key for live form id form-id-missing'\n )\n })\n})\n\ndescribe('formatPaymentDate', () => {\n it('should format ISO date string to en-GB format', () => {\n const result = formatPaymentDate('2025-11-10T17:01:29.000Z')\n expect(result).toBe('10 November 2025 5:01pm')\n })\n})\n\ndescribe('formatCurrency', () => {\n it('should format whole number with currency symbol', () => {\n expect(formatCurrency(10)).toBe('£10.00')\n })\n\n it('should format decimal amount with currency symbol', () => {\n expect(formatCurrency(99.5)).toBe('£99.50')\n })\n\n it('should format large amounts with thousand separators', () => {\n expect(formatCurrency(1234.56)).toBe('£1,234.56')\n })\n\n it('should format very large amounts with thousand separators', () => {\n expect(formatCurrency(20000)).toBe('£20,000.00')\n })\n})\n"],"mappings":"AAAA,SAASA,MAAM;AACf,SACEC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB;AAGlBC,QAAQ,CAAC,kBAAkB,EAAE,MAAM;EACjCJ,MAAM,CAACK,GAAG,CAAC,2BAA2B,EAAE,cAAc,CAAC;EACvD,MAAMC,MAAM,GAAG,SAAS;EACxBC,OAAO,CAACC,GAAG,CAAC,uCAAuC,CAAC,GAAG,cAAc;EACrED,OAAO,CAACC,GAAG,CAAC,uCAAuC,CAAC,GAAG,cAAc;EAErEC,EAAE,CAAC,yCAAyC,EAAE,MAAM;IAClD,MAAMC,MAAM,GAAGP,gBAAgB,CAAC,KAAK,EAAEG,MAAM,CAAC;IAC9CK,MAAM,CAACD,MAAM,CAAC,CAACE,IAAI,CAAC,cAAc,CAAC;EACrC,CAAC,CAAC;EAEFH,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9C,MAAMC,MAAM,GAAGP,gBAAgB,CAAC,IAAI,EAAEG,MAAM,CAAC;IAC7CK,MAAM,CAACD,MAAM,CAAC,CAACE,IAAI,CAAC,cAAc,CAAC;EACrC,CAAC,CAAC;EAEFH,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9CE,MAAM,CAAC,MAAMR,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAACU,OAAO,CAC9D,0DACF,CAAC;EACH,CAAC,CAAC;EAEFJ,EAAE,CAAC,qCAAqC,EAAE,MAAM;IAC9CE,MAAM,CAAC,MAAMR,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAACU,OAAO,CAC7D,0DACF,CAAC;EACH,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFT,QAAQ,CAAC,mBAAmB,EAAE,MAAM;EAClCK,EAAE,CAAC,+CAA+C,EAAE,MAAM;IACxD,MAAMK,MAAM,GAAGZ,iBAAiB,CAAC,0BAA0B,CAAC;IAC5DS,MAAM,CAACG,MAAM,CAAC,CAACF,IAAI,CAAC,yBAAyB,CAAC;EAChD,CAAC,CAAC;AACJ,CAAC,CAAC;AAEFR,QAAQ,CAAC,gBAAgB,EAAE,MAAM;EAC/BK,EAAE,CAAC,iDAAiD,EAAE,MAAM;IAC1DE,MAAM,CAACV,cAAc,CAAC,EAAE,CAAC,CAAC,CAACW,IAAI,CAAC,QAAQ,CAAC;EAC3C,CAAC,CAAC;EAEFH,EAAE,CAAC,mDAAmD,EAAE,MAAM;IAC5DE,MAAM,CAACV,cAAc,CAAC,IAAI,CAAC,CAAC,CAACW,IAAI,CAAC,QAAQ,CAAC;EAC7C,CAAC,CAAC;EAEFH,EAAE,CAAC,sDAAsD,EAAE,MAAM;IAC/DE,MAAM,CAACV,cAAc,CAAC,OAAO,CAAC,CAAC,CAACW,IAAI,CAAC,WAAW,CAAC;EACnD,CAAC,CAAC;EAEFH,EAAE,CAAC,2DAA2D,EAAE,MAAM;IACpEE,MAAM,CAACV,cAAc,CAAC,KAAK,CAAC,CAAC,CAACW,IAAI,CAAC,YAAY,CAAC;EAClD,CAAC,CAAC;AACJ,CAAC,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.0.51",
3
+ "version": "4.0.52",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -27,7 +27,7 @@ pages:
27
27
  type: PaymentField
28
28
  options:
29
29
  required: true
30
- amount: 300
30
+ amount: 10000
31
31
  description: Processing fee for your application.
32
32
  next:
33
33
  - path: '/summary'
@@ -196,7 +196,7 @@ describe('PaymentField', () => {
196
196
  label: { text: def.title },
197
197
  name: 'myComponent',
198
198
  id: 'myComponent',
199
- amount: '100.00',
199
+ amount: '£100.00',
200
200
  attributes: {},
201
201
  description: 'Test payment description'
202
202
  })
@@ -220,7 +220,7 @@ describe('PaymentField', () => {
220
220
  label: { text: def.title },
221
221
  name: 'myComponent',
222
222
  id: 'myComponent',
223
- amount: '100.00',
223
+ amount: '£100.00',
224
224
  attributes: {},
225
225
  description: 'Test payment description'
226
226
  })
@@ -29,7 +29,10 @@ import {
29
29
  type FormSubmissionError,
30
30
  type FormSubmissionState
31
31
  } from '~/src/server/plugins/engine/types.js'
32
- import { createPaymentService } from '~/src/server/plugins/payment/helper.js'
32
+ import {
33
+ createPaymentService,
34
+ formatCurrency
35
+ } from '~/src/server/plugins/payment/helper.js'
33
36
 
34
37
  export class PaymentField extends FormComponent {
35
38
  declare options: PaymentFieldComponent['options']
@@ -91,7 +94,7 @@ export class PaymentField extends FormComponent {
91
94
  return ''
92
95
  }
93
96
 
94
- return `£${value.amount.toFixed(2)} - ${value.description}`
97
+ return `${formatCurrency(value.amount)} - ${value.description}`
95
98
  }
96
99
 
97
100
  getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
@@ -105,11 +108,9 @@ export class PaymentField extends FormComponent {
105
108
  // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.
106
109
  const amount = paymentState?.amount ?? this.options.amount
107
110
 
108
- const formattedAmount = amount.toFixed(2)
109
-
110
111
  return {
111
112
  ...viewModel,
112
- amount: formattedAmount,
113
+ amount: formatCurrency(amount),
113
114
  description: this.options.description,
114
115
  paymentState
115
116
  }
@@ -153,7 +154,7 @@ export class PaymentField extends FormComponent {
153
154
 
154
155
  getContextValueFromState(state: FormSubmissionState) {
155
156
  return this.isPaymentState(state)
156
- ? `Reference: ${state.reference}\nAmount: ${state.amount.toFixed(2)}`
157
+ ? `Reference: ${state.reference}\nAmount: ${formatCurrency(state.amount)}`
157
158
  : ''
158
159
  }
159
160
 
@@ -16,7 +16,7 @@ import {
16
16
  } from '~/src/server/plugins/engine/models/types.js'
17
17
  import { type FormContext } from '~/src/server/plugins/engine/types.js'
18
18
  import {
19
- formatPaymentAmount,
19
+ formatCurrency,
20
20
  formatPaymentDate
21
21
  } from '~/src/server/plugins/payment/helper.js'
22
22
 
@@ -115,7 +115,7 @@ function appendPaymentSection(paymentItems: DetailItem[], lines: string[]) {
115
115
  return
116
116
  }
117
117
 
118
- const formattedAmount = formatPaymentAmount(paymentState.amount)
118
+ const formattedAmount = formatCurrency(paymentState.amount)
119
119
  const dateOfPayment = paymentState.preAuth?.createdAt
120
120
  ? formatPaymentDate(paymentState.preAuth.createdAt)
121
121
  : ''
@@ -46,7 +46,7 @@ import {
46
46
  } from '~/src/server/plugins/engine/types.js'
47
47
  import {
48
48
  DEFAULT_PAYMENT_HELP_URL,
49
- formatPaymentAmount,
49
+ formatCurrency,
50
50
  formatPaymentDate
51
51
  } from '~/src/server/plugins/payment/helper.js'
52
52
  import {
@@ -125,7 +125,7 @@ export class SummaryPageController extends QuestionPageController {
125
125
  },
126
126
  {
127
127
  key: { text: 'Total amount' },
128
- value: { text: formatPaymentAmount(paymentState.amount) }
128
+ value: { text: formatCurrency(paymentState.amount) }
129
129
  },
130
130
  {
131
131
  key: { text: 'Reference' },
@@ -7,7 +7,7 @@ import {
7
7
  type DetailItemField
8
8
  } from '~/src/server/plugins/engine/models/types.js'
9
9
  import {
10
- formatPaymentAmount,
10
+ formatCurrency,
11
11
  formatPaymentDate
12
12
  } from '~/src/server/plugins/payment/helper.js'
13
13
 
@@ -71,7 +71,7 @@ export function buildPaymentRecords(item: DetailItemField): SubmitRecord[] {
71
71
  {
72
72
  name: `${item.name}_paymentAmount`,
73
73
  title: 'Payment amount',
74
- value: formatPaymentAmount(paymentState.amount)
74
+ value: formatCurrency(paymentState.amount)
75
75
  },
76
76
  {
77
77
  name: `${item.name}_paymentReference`,
@@ -28,7 +28,7 @@
28
28
  <p class="govuk-body">You can submit the form after you have added your payment details.</p>
29
29
 
30
30
  <p class="govuk-body govuk-!-margin-bottom-1">Total amount:</p>
31
- <p class="govuk-heading-l govuk-!-margin-bottom-4 govuk-!-padding-top-0"{{ amount }}</p>
31
+ <p class="govuk-heading-l govuk-!-margin-bottom-4 govuk-!-padding-top-0">{{ amount }}</p>
32
32
 
33
33
  {{ govukButton({
34
34
  text: "Add payment details",
@@ -47,10 +47,17 @@ export function formatPaymentDate(isoString) {
47
47
  }
48
48
 
49
49
  /**
50
- * Formats a payment amount with two decimal places
50
+ * Formats a currency amount with thousand separators and two decimal places
51
51
  * @param {number} amount - amount in pounds
52
- * @returns {string} Formatted amount (e.g., "£10.00")
52
+ * @param {'en-GB'} [locale] - locale for formatting
53
+ * @param {'GBP'} [currency] - currency code
54
+ * @returns {string} Formatted amount (e.g., "£1,234.56")
53
55
  */
54
- export function formatPaymentAmount(amount) {
55
- return `£${amount.toFixed(2)}`
56
+ export function formatCurrency(amount, locale = 'en-GB', currency = 'GBP') {
57
+ const formatter = new Intl.NumberFormat(locale, {
58
+ style: 'currency',
59
+ currency
60
+ })
61
+
62
+ return formatter.format(amount)
56
63
  }
@@ -1,6 +1,6 @@
1
1
  import { config } from '~/src/config/index.js'
2
2
  import {
3
- formatPaymentAmount,
3
+ formatCurrency,
4
4
  formatPaymentDate,
5
5
  getPaymentApiKey
6
6
  } from '~/src/server/plugins/payment/helper.js'
@@ -41,12 +41,20 @@ describe('formatPaymentDate', () => {
41
41
  })
42
42
  })
43
43
 
44
- describe('formatPaymentAmount', () => {
45
- it('should format whole number with two decimal places', () => {
46
- expect(formatPaymentAmount(10)).toBe('£10.00')
44
+ describe('formatCurrency', () => {
45
+ it('should format whole number with currency symbol', () => {
46
+ expect(formatCurrency(10)).toBe('£10.00')
47
47
  })
48
48
 
49
- it('should format decimal amount', () => {
50
- expect(formatPaymentAmount(99.5)).toBe('£99.50')
49
+ it('should format decimal amount with currency symbol', () => {
50
+ expect(formatCurrency(99.5)).toBe('£99.50')
51
+ })
52
+
53
+ it('should format large amounts with thousand separators', () => {
54
+ expect(formatCurrency(1234.56)).toBe('£1,234.56')
55
+ })
56
+
57
+ it('should format very large amounts with thousand separators', () => {
58
+ expect(formatCurrency(20000)).toBe('£20,000.00')
51
59
  })
52
60
  })