@defra/forms-engine-plugin 4.0.50 → 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.
- package/.server/server/forms/payment-test.yaml +1 -1
- package/.server/server/plugins/engine/components/PaymentField.js +4 -5
- package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js +2 -2
- package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -2
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js +2 -2
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
- package/.server/server/plugins/engine/views/components/paymentfield.html +1 -1
- package/.server/server/plugins/payment/helper.d.ts +5 -3
- package/.server/server/plugins/payment/helper.js +10 -4
- package/.server/server/plugins/payment/helper.js.map +1 -1
- package/.server/server/plugins/payment/helper.test.js +12 -6
- package/.server/server/plugins/payment/helper.test.js.map +1 -1
- package/.server/server/plugins/payment/service.js +3 -3
- package/.server/server/plugins/payment/service.js.map +1 -1
- package/package.json +1 -1
- package/src/server/forms/payment-test.yaml +1 -1
- package/src/server/plugins/engine/components/PaymentField.test.ts +2 -2
- package/src/server/plugins/engine/components/PaymentField.ts +7 -6
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +2 -2
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +2 -2
- package/src/server/plugins/engine/pageControllers/helpers/submission.ts +2 -2
- package/src/server/plugins/engine/views/components/paymentfield.html +1 -1
- package/src/server/plugins/payment/helper.js +11 -4
- package/src/server/plugins/payment/helper.test.js +14 -6
- package/src/server/plugins/payment/service.js +3 -3
|
@@ -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
|
|
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:
|
|
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
|
|
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 {
|
|
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 =
|
|
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","
|
|
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":"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","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,
|
|
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:
|
|
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 {
|
|
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:
|
|
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","
|
|
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"
|
|
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
|
|
24
|
+
* Formats a currency amount with thousand separators and two decimal places
|
|
25
25
|
* @param {number} amount - amount in pounds
|
|
26
|
-
* @
|
|
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
|
|
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
|
|
42
|
+
* Formats a currency amount with thousand separators and two decimal places
|
|
43
43
|
* @param {number} amount - amount in pounds
|
|
44
|
-
* @
|
|
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
|
|
47
|
-
|
|
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","
|
|
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 {
|
|
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('
|
|
30
|
-
it('should format whole number with
|
|
31
|
-
expect(
|
|
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(
|
|
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","
|
|
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":[]}
|
|
@@ -44,7 +44,7 @@ export class PaymentService {
|
|
|
44
44
|
});
|
|
45
45
|
logger.info({
|
|
46
46
|
event: {
|
|
47
|
-
|
|
47
|
+
category: 'payment',
|
|
48
48
|
action: 'create-payment',
|
|
49
49
|
outcome: 'success',
|
|
50
50
|
reason: `amount=${amount}`,
|
|
@@ -75,7 +75,7 @@ export class PaymentService {
|
|
|
75
75
|
const state = response.payload.state;
|
|
76
76
|
logger.info({
|
|
77
77
|
event: {
|
|
78
|
-
|
|
78
|
+
category: 'payment',
|
|
79
79
|
action: 'get-payment-status',
|
|
80
80
|
outcome: state.status === 'capturable' || state.status === 'success' ? 'success' : 'failure',
|
|
81
81
|
reason: `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,
|
|
@@ -111,7 +111,7 @@ export class PaymentService {
|
|
|
111
111
|
if (statusCode === StatusCodes.OK || statusCode === StatusCodes.NO_CONTENT) {
|
|
112
112
|
logger.info({
|
|
113
113
|
event: {
|
|
114
|
-
|
|
114
|
+
category: 'payment',
|
|
115
115
|
action: 'capture-payment',
|
|
116
116
|
outcome: 'success',
|
|
117
117
|
reason: `amount=${amount}`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":["StatusCodes","createLogger","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","metadata","response","postToPayProvider","return_url","delayed_capture","info","event","module","action","outcome","reason","payment_id","paymentId","paymentUrl","_links","next_url","href","getPaymentStatus","getByType","headers","json","error","errorMessage","Error","message","JSON","stringify","state","payload","status","code","email","err","capturePayment","statusCode","res","OK","NO_CONTENT","postJsonByType"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {{ formId: string, slug: string }} metadata\n */\n async createPayment(amount, description, returnUrl, reference, metadata) {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n {\n event: {\n module: 'payment',\n action: 'create-payment',\n outcome: 'success',\n reason: `amount=${amount}`,\n reference: response.payment_id\n }\n },\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n }\n\n /**\n * @param {string} paymentId\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n {\n event: {\n module: 'payment',\n action: 'get-payment-status',\n outcome:\n state.status === 'capturable' || state.status === 'success'\n ? 'success'\n : 'failure',\n reason: `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,\n reference: paymentId\n }\n },\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n module: 'payment',\n action: 'capture-payment',\n outcome: 'success',\n reason: `amount=${amount}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"service.js","names":["StatusCodes","createLogger","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","metadata","response","postToPayProvider","return_url","delayed_capture","info","event","category","action","outcome","reason","payment_id","paymentId","paymentUrl","_links","next_url","href","getPaymentStatus","getByType","headers","json","error","errorMessage","Error","message","JSON","stringify","state","payload","status","code","email","err","capturePayment","statusCode","res","OK","NO_CONTENT","postJsonByType"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {{ formId: string, slug: string }} metadata\n */\n async createPayment(amount, description, returnUrl, reference, metadata) {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'create-payment',\n outcome: 'success',\n reason: `amount=${amount}`,\n reference: response.payment_id\n }\n },\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n }\n\n /**\n * @param {string} paymentId\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'get-payment-status',\n outcome:\n state.status === 'capturable' || state.status === 'success'\n ? 'success'\n : 'failure',\n reason: `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,\n reference: paymentId\n }\n },\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'capture-payment',\n outcome: 'success',\n reason: `amount=${amount}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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"
|
|
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
|
|
50
|
+
* Formats a currency amount with thousand separators and two decimal places
|
|
51
51
|
* @param {number} amount - amount in pounds
|
|
52
|
-
* @
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
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('
|
|
45
|
-
it('should format whole number with
|
|
46
|
-
expect(
|
|
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(
|
|
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
|
})
|
|
@@ -50,7 +50,7 @@ export class PaymentService {
|
|
|
50
50
|
logger.info(
|
|
51
51
|
{
|
|
52
52
|
event: {
|
|
53
|
-
|
|
53
|
+
category: 'payment',
|
|
54
54
|
action: 'create-payment',
|
|
55
55
|
outcome: 'success',
|
|
56
56
|
reason: `amount=${amount}`,
|
|
@@ -94,7 +94,7 @@ export class PaymentService {
|
|
|
94
94
|
logger.info(
|
|
95
95
|
{
|
|
96
96
|
event: {
|
|
97
|
-
|
|
97
|
+
category: 'payment',
|
|
98
98
|
action: 'get-payment-status',
|
|
99
99
|
outcome:
|
|
100
100
|
state.status === 'capturable' || state.status === 'success'
|
|
@@ -148,7 +148,7 @@ export class PaymentService {
|
|
|
148
148
|
logger.info(
|
|
149
149
|
{
|
|
150
150
|
event: {
|
|
151
|
-
|
|
151
|
+
category: 'payment',
|
|
152
152
|
action: 'capture-payment',
|
|
153
153
|
outcome: 'success',
|
|
154
154
|
reason: `amount=${amount}`,
|