@defra/forms-engine-plugin 4.0.53 → 4.0.54
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/plugins/engine/components/PaymentField.js +2 -2
- package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
- package/.server/server/plugins/engine/routes/payment-helper.d.ts +22 -0
- package/.server/server/plugins/engine/routes/payment-helper.js +29 -1
- package/.server/server/plugins/engine/routes/payment-helper.js.map +1 -1
- package/.server/server/plugins/engine/routes/payment-helper.test.js +27 -1
- package/.server/server/plugins/engine/routes/payment-helper.test.js.map +1 -1
- package/.server/server/plugins/engine/routes/payment.js +23 -1
- package/.server/server/plugins/engine/routes/payment.js.map +1 -1
- package/.server/server/plugins/payment/service.d.ts +4 -2
- package/.server/server/plugins/payment/service.js +8 -21
- package/.server/server/plugins/payment/service.js.map +1 -1
- package/.server/server/plugins/payment/service.test.js +5 -5
- package/.server/server/plugins/payment/service.test.js.map +1 -1
- package/package.json +1 -1
- package/src/server/plugins/engine/components/PaymentField.ts +5 -1
- package/src/server/plugins/engine/routes/payment-helper.js +38 -1
- package/src/server/plugins/engine/routes/payment-helper.test.js +44 -1
- package/src/server/plugins/engine/routes/payment.js +46 -1
- package/src/server/plugins/payment/service.js +32 -24
- package/src/server/plugins/payment/service.test.js +8 -2
|
@@ -142,7 +142,7 @@ export class PaymentField extends FormComponent {
|
|
|
142
142
|
const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`;
|
|
143
143
|
const paymentPageUrl = args.sourceUrl;
|
|
144
144
|
const amountInPence = Math.round(amount * 100);
|
|
145
|
-
const payment = await paymentService.createPayment(amountInPence, description, payCallbackUrl, reference, {
|
|
145
|
+
const payment = await paymentService.createPayment(amountInPence, description, payCallbackUrl, reference, isLivePayment, {
|
|
146
146
|
formId,
|
|
147
147
|
slug
|
|
148
148
|
});
|
|
@@ -184,7 +184,7 @@ export class PaymentField extends FormComponent {
|
|
|
184
184
|
/**
|
|
185
185
|
* @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
|
|
186
186
|
*/
|
|
187
|
-
const status = await paymentService.getPaymentStatus(paymentId);
|
|
187
|
+
const status = await paymentService.getPaymentStatus(paymentId, isLivePayment);
|
|
188
188
|
PaymentSubmissionError.checkPaymentAmount(status.amount, this.options.amount, this);
|
|
189
189
|
if (status.state.status === 'success') {
|
|
190
190
|
await this.markPaymentCaptured(request, paymentState);
|
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
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 isLivePayment,\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(\n paymentId,\n isLivePayment\n )\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":[]}
|
|
@@ -9,6 +9,28 @@ export function getPaymentContext(request: Request, uuid: string): Promise<{
|
|
|
9
9
|
sessionKey: string;
|
|
10
10
|
paymentStatus: GetPaymentResponse;
|
|
11
11
|
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Builds an object for logging payment information
|
|
14
|
+
* @param {string} action
|
|
15
|
+
* @param {string} outcome
|
|
16
|
+
* @param {string} reason
|
|
17
|
+
* @param {boolean} isLivePayment
|
|
18
|
+
* @param {string} paymentId
|
|
19
|
+
*/
|
|
20
|
+
export function buildPaymentInfo(action: string, outcome: string, reason: string, isLivePayment: boolean, paymentId: string): {
|
|
21
|
+
event: {
|
|
22
|
+
category: string;
|
|
23
|
+
action: string;
|
|
24
|
+
outcome: string;
|
|
25
|
+
reason: string;
|
|
26
|
+
type: string;
|
|
27
|
+
reference: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @param {number} amount
|
|
32
|
+
*/
|
|
33
|
+
export function convertPenceToPounds(amount: number): string;
|
|
12
34
|
import type { Request } from '@hapi/hapi';
|
|
13
35
|
import type { PaymentSessionData } from '~/src/server/plugins/payment/types.js';
|
|
14
36
|
import type { GetPaymentResponse } from '~/src/server/plugins/payment/types.js';
|
|
@@ -26,7 +26,7 @@ export async function getPaymentContext(request, uuid) {
|
|
|
26
26
|
}
|
|
27
27
|
const apiKey = getPaymentApiKey(isLivePayment, formId);
|
|
28
28
|
const paymentService = new PaymentService(apiKey);
|
|
29
|
-
const paymentStatus = await paymentService.getPaymentStatus(paymentId);
|
|
29
|
+
const paymentStatus = await paymentService.getPaymentStatus(paymentId, isLivePayment);
|
|
30
30
|
return {
|
|
31
31
|
session,
|
|
32
32
|
sessionKey,
|
|
@@ -34,6 +34,34 @@ export async function getPaymentContext(request, uuid) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Builds an object for logging payment information
|
|
39
|
+
* @param {string} action
|
|
40
|
+
* @param {string} outcome
|
|
41
|
+
* @param {string} reason
|
|
42
|
+
* @param {boolean} isLivePayment
|
|
43
|
+
* @param {string} paymentId
|
|
44
|
+
*/
|
|
45
|
+
export function buildPaymentInfo(action, outcome, reason, isLivePayment, paymentId) {
|
|
46
|
+
return {
|
|
47
|
+
event: {
|
|
48
|
+
category: 'payment',
|
|
49
|
+
action,
|
|
50
|
+
outcome,
|
|
51
|
+
reason,
|
|
52
|
+
type: isLivePayment ? 'live' : 'test',
|
|
53
|
+
reference: paymentId
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {number} amount
|
|
60
|
+
*/
|
|
61
|
+
export function convertPenceToPounds(amount) {
|
|
62
|
+
return `${amount / 100}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
37
65
|
/**
|
|
38
66
|
* @import { Request } from '@hapi/hapi'
|
|
39
67
|
* @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-helper.js","names":["Boom","PAYMENT_SESSION_PREFIX","getPaymentApiKey","PaymentService","getPaymentContext","request","uuid","sessionKey","session","yar","get","badRequest","paymentId","isLivePayment","formId","apiKey","paymentService","paymentStatus","getPaymentStatus"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.js"],"sourcesContent":["import Boom from '@hapi/boom'\n\nimport { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'\nimport { PaymentService } from '~/src/server/plugins/payment/service.js'\n\n/**\n * Validates session data and retrieves payment status\n * @param {Request} request - the request\n * @param {string} uuid - the payment UUID\n * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}\n */\nexport async function getPaymentContext(request, uuid) {\n const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`\n const session = /** @type {PaymentSessionData | null} */ (\n request.yar.get(sessionKey)\n )\n\n if (!session) {\n throw Boom.badRequest(`No payment session found for uuid=${uuid}`)\n }\n\n const { paymentId, isLivePayment, formId } = session\n\n if (!paymentId) {\n throw Boom.badRequest('No paymentId in session')\n }\n\n const apiKey = getPaymentApiKey(isLivePayment, formId)\n const paymentService = new PaymentService(apiKey)\n const paymentStatus = await paymentService.getPaymentStatus(paymentId)\n\n return { session, sessionKey, paymentStatus }\n}\n\n/**\n * @import { Request } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,YAAY;AAE7B,SAASC,sBAAsB;AAC/B,SAASC,gBAAgB;AACzB,SAASC,cAAc;;AAEvB;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,iBAAiBA,CAACC,OAAO,EAAEC,IAAI,EAAE;EACrD,MAAMC,UAAU,GAAG,GAAGN,sBAAsB,GAAGK,IAAI,EAAE;EACrD,MAAME,OAAO,GAAG;EACdH,OAAO,CAACI,GAAG,CAACC,GAAG,CAACH,UAAU,CAC3B;EAED,IAAI,CAACC,OAAO,EAAE;IACZ,MAAMR,IAAI,CAACW,UAAU,CAAC,qCAAqCL,IAAI,EAAE,CAAC;EACpE;EAEA,MAAM;IAAEM,SAAS;IAAEC,aAAa;IAAEC;EAAO,CAAC,GAAGN,OAAO;EAEpD,IAAI,CAACI,SAAS,EAAE;IACd,MAAMZ,IAAI,CAACW,UAAU,CAAC,yBAAyB,CAAC;EAClD;EAEA,MAAMI,MAAM,GAAGb,gBAAgB,CAACW,aAAa,EAAEC,MAAM,CAAC;EACtD,MAAME,cAAc,GAAG,IAAIb,cAAc,CAACY,MAAM,CAAC;EACjD,MAAME,aAAa,GAAG,MAAMD,cAAc,CAACE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"payment-helper.js","names":["Boom","PAYMENT_SESSION_PREFIX","getPaymentApiKey","PaymentService","getPaymentContext","request","uuid","sessionKey","session","yar","get","badRequest","paymentId","isLivePayment","formId","apiKey","paymentService","paymentStatus","getPaymentStatus","buildPaymentInfo","action","outcome","reason","event","category","type","reference","convertPenceToPounds","amount"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.js"],"sourcesContent":["import Boom from '@hapi/boom'\n\nimport { PAYMENT_SESSION_PREFIX } from '~/src/server/plugins/engine/routes/payment.js'\nimport { getPaymentApiKey } from '~/src/server/plugins/payment/helper.js'\nimport { PaymentService } from '~/src/server/plugins/payment/service.js'\n\n/**\n * Validates session data and retrieves payment status\n * @param {Request} request - the request\n * @param {string} uuid - the payment UUID\n * @returns {Promise<{ session: PaymentSessionData, sessionKey: string, paymentStatus: GetPaymentResponse }>}\n */\nexport async function getPaymentContext(request, uuid) {\n const sessionKey = `${PAYMENT_SESSION_PREFIX}${uuid}`\n const session = /** @type {PaymentSessionData | null} */ (\n request.yar.get(sessionKey)\n )\n\n if (!session) {\n throw Boom.badRequest(`No payment session found for uuid=${uuid}`)\n }\n\n const { paymentId, isLivePayment, formId } = session\n\n if (!paymentId) {\n throw Boom.badRequest('No paymentId in session')\n }\n\n const apiKey = getPaymentApiKey(isLivePayment, formId)\n const paymentService = new PaymentService(apiKey)\n const paymentStatus = await paymentService.getPaymentStatus(\n paymentId,\n isLivePayment\n )\n\n return { session, sessionKey, paymentStatus }\n}\n\n/**\n * Builds an object for logging payment information\n * @param {string} action\n * @param {string} outcome\n * @param {string} reason\n * @param {boolean} isLivePayment\n * @param {string} paymentId\n */\nexport function buildPaymentInfo(\n action,\n outcome,\n reason,\n isLivePayment,\n paymentId\n) {\n return {\n event: {\n category: 'payment',\n action,\n outcome,\n reason,\n type: isLivePayment ? 'live' : 'test',\n reference: paymentId\n }\n }\n}\n\n/**\n * @param {number} amount\n */\nexport function convertPenceToPounds(amount) {\n return `${amount / 100}`\n}\n\n/**\n * @import { Request } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"AAAA,OAAOA,IAAI,MAAM,YAAY;AAE7B,SAASC,sBAAsB;AAC/B,SAASC,gBAAgB;AACzB,SAASC,cAAc;;AAEvB;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,eAAeC,iBAAiBA,CAACC,OAAO,EAAEC,IAAI,EAAE;EACrD,MAAMC,UAAU,GAAG,GAAGN,sBAAsB,GAAGK,IAAI,EAAE;EACrD,MAAME,OAAO,GAAG;EACdH,OAAO,CAACI,GAAG,CAACC,GAAG,CAACH,UAAU,CAC3B;EAED,IAAI,CAACC,OAAO,EAAE;IACZ,MAAMR,IAAI,CAACW,UAAU,CAAC,qCAAqCL,IAAI,EAAE,CAAC;EACpE;EAEA,MAAM;IAAEM,SAAS;IAAEC,aAAa;IAAEC;EAAO,CAAC,GAAGN,OAAO;EAEpD,IAAI,CAACI,SAAS,EAAE;IACd,MAAMZ,IAAI,CAACW,UAAU,CAAC,yBAAyB,CAAC;EAClD;EAEA,MAAMI,MAAM,GAAGb,gBAAgB,CAACW,aAAa,EAAEC,MAAM,CAAC;EACtD,MAAME,cAAc,GAAG,IAAIb,cAAc,CAACY,MAAM,CAAC;EACjD,MAAME,aAAa,GAAG,MAAMD,cAAc,CAACE,gBAAgB,CACzDN,SAAS,EACTC,aACF,CAAC;EAED,OAAO;IAAEL,OAAO;IAAED,UAAU;IAAEU;EAAc,CAAC;AAC/C;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,gBAAgBA,CAC9BC,MAAM,EACNC,OAAO,EACPC,MAAM,EACNT,aAAa,EACbD,SAAS,EACT;EACA,OAAO;IACLW,KAAK,EAAE;MACLC,QAAQ,EAAE,SAAS;MACnBJ,MAAM;MACNC,OAAO;MACPC,MAAM;MACNG,IAAI,EAAEZ,aAAa,GAAG,MAAM,GAAG,MAAM;MACrCa,SAAS,EAAEd;IACb;EACF,CAAC;AACH;;AAEA;AACA;AACA;AACA,OAAO,SAASe,oBAAoBA,CAACC,MAAM,EAAE;EAC3C,OAAO,GAAGA,MAAM,GAAG,GAAG,EAAE;AAC1B;;AAEA;AACA;AACA;AACA","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPaymentContext } from "./payment-helper.js";
|
|
1
|
+
import { buildPaymentInfo, getPaymentContext } from "./payment-helper.js";
|
|
2
2
|
import { get } from "../../../services/httpService.js";
|
|
3
3
|
jest.mock("../../../services/httpService.ts");
|
|
4
4
|
describe('payment helper', () => {
|
|
@@ -73,6 +73,32 @@ describe('payment helper', () => {
|
|
|
73
73
|
sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
|
+
it('should create logging info for a test payment', () => {
|
|
77
|
+
const res = buildPaymentInfo('action1', 'outcome1', 'reason1', false, 'pay-123');
|
|
78
|
+
expect(res).toEqual({
|
|
79
|
+
event: {
|
|
80
|
+
category: 'payment',
|
|
81
|
+
action: 'action1',
|
|
82
|
+
outcome: 'outcome1',
|
|
83
|
+
reason: 'reason1',
|
|
84
|
+
type: 'test',
|
|
85
|
+
reference: 'pay-123'
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should create logging info for a live payment', () => {
|
|
90
|
+
const res = buildPaymentInfo('action2', 'outcome2', 'reason2', true, 'pay-123');
|
|
91
|
+
expect(res).toEqual({
|
|
92
|
+
event: {
|
|
93
|
+
category: 'payment',
|
|
94
|
+
action: 'action2',
|
|
95
|
+
outcome: 'outcome2',
|
|
96
|
+
reason: 'reason2',
|
|
97
|
+
type: 'live',
|
|
98
|
+
reference: 'pay-123'
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
76
102
|
});
|
|
77
103
|
|
|
78
104
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment-helper.test.js","names":["getPaymentContext","get","jest","mock","describe","uuid","it","mockRequest","yar","fn","mockReturnValueOnce","undefined","expect","rejects","toThrow","paymentId","isLivePayment","formId","getPaymentStatusApiResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","toEqual","paymentStatus","session","sessionKey"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.test.js"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"payment-helper.test.js","names":["buildPaymentInfo","getPaymentContext","get","jest","mock","describe","uuid","it","mockRequest","yar","fn","mockReturnValueOnce","undefined","expect","rejects","toThrow","paymentId","isLivePayment","formId","getPaymentStatusApiResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","toEqual","paymentStatus","session","sessionKey","event","category","action","outcome","reason","type","reference"],"sources":["../../../../../src/server/plugins/engine/routes/payment-helper.test.js"],"sourcesContent":["import {\n buildPaymentInfo,\n getPaymentContext\n} from '~/src/server/plugins/engine/routes/payment-helper.js'\nimport { get } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment helper', () => {\n const uuid = '5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n it('should throw if no session', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce(undefined)\n }\n }\n // @ts-expect-error - partial request mock\n await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(\n 'No payment session found for uuid=5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n )\n })\n\n it('should throw if no payment id', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce({})\n }\n }\n // @ts-expect-error - partial request mock\n await expect(() => getPaymentContext(mockRequest, uuid)).rejects.toThrow(\n 'No paymentId in session'\n )\n })\n\n it('should get context successfully', async () => {\n const mockRequest = {\n yar: {\n get: jest.fn().mockReturnValueOnce({\n paymentId: 'payment-id',\n isLivePayment: false,\n formId: 'formid'\n })\n }\n }\n\n const getPaymentStatusApiResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusApiResult,\n error: undefined\n })\n\n // @ts-expect-error - partial request mock\n const res = await getPaymentContext(mockRequest, uuid)\n expect(res).toEqual({\n paymentStatus: {\n paymentId: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n },\n session: {\n formId: 'formid',\n isLivePayment: false,\n paymentId: 'payment-id'\n },\n sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'\n })\n })\n\n it('should create logging info for a test payment', () => {\n const res = buildPaymentInfo(\n 'action1',\n 'outcome1',\n 'reason1',\n false,\n 'pay-123'\n )\n expect(res).toEqual({\n event: {\n category: 'payment',\n action: 'action1',\n outcome: 'outcome1',\n reason: 'reason1',\n type: 'test',\n reference: 'pay-123'\n }\n })\n })\n\n it('should create logging info for a live payment', () => {\n const res = buildPaymentInfo(\n 'action2',\n 'outcome2',\n 'reason2',\n true,\n 'pay-123'\n )\n expect(res).toEqual({\n event: {\n category: 'payment',\n action: 'action2',\n outcome: 'outcome2',\n reason: 'reason2',\n type: 'live',\n reference: 'pay-123'\n }\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import Boom from '@hapi/boom';
|
|
2
2
|
import { StatusCodes } from 'http-status-codes';
|
|
3
3
|
import Joi from 'joi';
|
|
4
|
+
import { createLogger } from "../../../common/helpers/logging/logger.js";
|
|
4
5
|
import { EXTERNAL_STATE_APPENDAGE } from "../../../constants.js";
|
|
5
|
-
import { getPaymentContext } from "./payment-helper.js";
|
|
6
|
+
import { buildPaymentInfo, convertPenceToPounds, getPaymentContext } from "./payment-helper.js";
|
|
6
7
|
export const PAYMENT_RETURN_PATH = '/payment-callback';
|
|
7
8
|
export const PAYMENT_SESSION_PREFIX = 'payment-';
|
|
9
|
+
const logger = createLogger();
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Flash form component state after successful payment
|
|
@@ -45,6 +47,24 @@ export function getRoutes() {
|
|
|
45
47
|
return [getReturnRoute()];
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Logs successful payment
|
|
52
|
+
* @param {PaymentSessionData} session - the session data
|
|
53
|
+
* @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay
|
|
54
|
+
*/
|
|
55
|
+
function logPaymentSuccess(session, paymentStatus) {
|
|
56
|
+
logger.info(buildPaymentInfo('pre-auth', 'success', `${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`, session.isLivePayment, paymentStatus.paymentId), `[payment] Successful pre-auth for paymentId=${paymentStatus.paymentId}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Logs failed/cancelled payment
|
|
61
|
+
* @param {PaymentSessionData} session - the session data
|
|
62
|
+
* @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay
|
|
63
|
+
*/
|
|
64
|
+
function logPaymentFailure(session, paymentStatus) {
|
|
65
|
+
logger.info(buildPaymentInfo('pre-auth', 'failed/cancelled', `${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`, session.isLivePayment, paymentStatus.paymentId), `[payment] Failed/cancelled pre-auth for paymentId=${paymentStatus.paymentId}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
/**
|
|
49
69
|
* Handles successful payment states (capturable/success)
|
|
50
70
|
* @param {Request} request - the request
|
|
@@ -99,10 +119,12 @@ function getReturnRoute() {
|
|
|
99
119
|
switch (status) {
|
|
100
120
|
case 'capturable':
|
|
101
121
|
case 'success':
|
|
122
|
+
logPaymentSuccess(session, paymentStatus);
|
|
102
123
|
return handlePaymentSuccess(request, h, session, sessionKey, paymentStatus);
|
|
103
124
|
case 'cancelled':
|
|
104
125
|
case 'failed':
|
|
105
126
|
case 'error':
|
|
127
|
+
logPaymentFailure(session, paymentStatus);
|
|
106
128
|
return handlePaymentFailure(request, h, session, sessionKey);
|
|
107
129
|
case 'created':
|
|
108
130
|
case 'started':
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"payment.js","names":["Boom","StatusCodes","Joi","EXTERNAL_STATE_APPENDAGE","getPaymentContext","PAYMENT_RETURN_PATH","PAYMENT_SESSION_PREFIX","flashComponentState","request","session","paymentStatus","paymentState","paymentId","reference","amount","description","uuid","formId","isLivePayment","payerEmail","email","preAuth","status","createdAt","Date","toISOString","appendage","component","componentName","data","yar","flash","getRoutes","getReturnRoute","handlePaymentSuccess","h","sessionKey","clear","redirect","returnUrl","code","SEE_OTHER","handlePaymentFailure","failureUrl","method","path","handler","query","state","nextUrl","_links","next_url","href","badRequest","unknownStatus","internal","options","validate","object","keys","string","required"],"sources":["../../../../../src/server/plugins/engine/routes/payment.js"],"sourcesContent":["import Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\nimport Joi from 'joi'\n\nimport { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'\nimport { getPaymentContext } from '~/src/server/plugins/engine/routes/payment-helper.js'\n\nexport const PAYMENT_RETURN_PATH = '/payment-callback'\nexport const PAYMENT_SESSION_PREFIX = 'payment-'\n\n/**\n * Flash form component state after successful payment\n * @param {Request} request - the request\n * @param {PaymentSessionData} session - the session data containing payment state\n * @param {GetPaymentResponse} paymentStatus - the payment status response from GOV.UK Pay\n */\nfunction flashComponentState(request, session, paymentStatus) {\n /** @type {PaymentState} */\n const paymentState = {\n paymentId: paymentStatus.paymentId,\n reference: session.reference,\n amount: session.amount,\n description: session.description,\n uuid: session.uuid,\n formId: session.formId,\n isLivePayment: session.isLivePayment,\n payerEmail: paymentStatus.email,\n preAuth: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n /** @type {ExternalStateAppendage} */\n const appendage = {\n component: session.componentName,\n data: /** @type {FormState} */ (/** @type {unknown} */ (paymentState))\n }\n\n request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)\n}\n\n/**\n * Gets the payment routes for handling GOV.UK Pay callbacks\n * @returns {ServerRoute[]}\n */\nexport function getRoutes() {\n return [getReturnRoute()]\n}\n\n/**\n * Handles successful payment states (capturable/success)\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay\n */\nfunction handlePaymentSuccess(request, h, session, sessionKey, paymentStatus) {\n flashComponentState(request, session, paymentStatus)\n request.yar.clear(sessionKey)\n return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Handles failed/cancelled/error payment states\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n */\nfunction handlePaymentFailure(request, h, session, sessionKey) {\n request.yar.clear(sessionKey)\n return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Route handler for payment return URL\n * This is called when GOV.UK Pay redirects the user back after payment\n * @returns {ServerRoute}\n */\nfunction getReturnRoute() {\n return {\n method: 'GET',\n path: PAYMENT_RETURN_PATH,\n async handler(request, h) {\n const { uuid } = /** @type {{ uuid: string }} */ (request.query)\n const { session, sessionKey, paymentStatus } = await getPaymentContext(\n request,\n uuid\n )\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const { status } = paymentStatus.state\n\n switch (status) {\n case 'capturable':\n case 'success':\n return handlePaymentSuccess(\n request,\n h,\n session,\n sessionKey,\n paymentStatus\n )\n\n case 'cancelled':\n case 'failed':\n case 'error':\n return handlePaymentFailure(request, h, session, sessionKey)\n\n case 'created':\n case 'started':\n case 'submitted': {\n const nextUrl = paymentStatus._links.next_url?.href\n\n if (!nextUrl) {\n throw Boom.badRequest(\n `Payment in state '${status}' but no next_url available`\n )\n }\n\n return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER)\n }\n\n default: {\n const unknownStatus = /** @type {string} */ (status)\n throw Boom.internal(`Unknown payment status: ${unknownStatus}`)\n }\n }\n },\n options: {\n validate: {\n query: Joi.object()\n .keys({\n uuid: Joi.string().uuid().required()\n })\n .required()\n }\n }\n }\n}\n\n/**\n * @import { Request, ResponseToolkit, ServerRoute } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n * @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\n * @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"payment.js","names":["Boom","StatusCodes","Joi","createLogger","EXTERNAL_STATE_APPENDAGE","buildPaymentInfo","convertPenceToPounds","getPaymentContext","PAYMENT_RETURN_PATH","PAYMENT_SESSION_PREFIX","logger","flashComponentState","request","session","paymentStatus","paymentState","paymentId","reference","amount","description","uuid","formId","isLivePayment","payerEmail","email","preAuth","status","createdAt","Date","toISOString","appendage","component","componentName","data","yar","flash","getRoutes","getReturnRoute","logPaymentSuccess","info","state","logPaymentFailure","handlePaymentSuccess","h","sessionKey","clear","redirect","returnUrl","code","SEE_OTHER","handlePaymentFailure","failureUrl","method","path","handler","query","nextUrl","_links","next_url","href","badRequest","unknownStatus","internal","options","validate","object","keys","string","required"],"sources":["../../../../../src/server/plugins/engine/routes/payment.js"],"sourcesContent":["import Boom from '@hapi/boom'\nimport { StatusCodes } from 'http-status-codes'\nimport Joi from 'joi'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'\nimport {\n buildPaymentInfo,\n convertPenceToPounds,\n getPaymentContext\n} from '~/src/server/plugins/engine/routes/payment-helper.js'\n\nexport const PAYMENT_RETURN_PATH = '/payment-callback'\nexport const PAYMENT_SESSION_PREFIX = 'payment-'\n\nconst logger = createLogger()\n\n/**\n * Flash form component state after successful payment\n * @param {Request} request - the request\n * @param {PaymentSessionData} session - the session data containing payment state\n * @param {GetPaymentResponse} paymentStatus - the payment status response from GOV.UK Pay\n */\nfunction flashComponentState(request, session, paymentStatus) {\n /** @type {PaymentState} */\n const paymentState = {\n paymentId: paymentStatus.paymentId,\n reference: session.reference,\n amount: session.amount,\n description: session.description,\n uuid: session.uuid,\n formId: session.formId,\n isLivePayment: session.isLivePayment,\n payerEmail: paymentStatus.email,\n preAuth: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n /** @type {ExternalStateAppendage} */\n const appendage = {\n component: session.componentName,\n data: /** @type {FormState} */ (/** @type {unknown} */ (paymentState))\n }\n\n request.yar.flash(EXTERNAL_STATE_APPENDAGE, appendage, true)\n}\n\n/**\n * Gets the payment routes for handling GOV.UK Pay callbacks\n * @returns {ServerRoute[]}\n */\nexport function getRoutes() {\n return [getReturnRoute()]\n}\n\n/**\n * Logs successful payment\n * @param {PaymentSessionData} session - the session data\n * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay\n */\nfunction logPaymentSuccess(session, paymentStatus) {\n logger.info(\n buildPaymentInfo(\n 'pre-auth',\n 'success',\n `${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`,\n session.isLivePayment,\n paymentStatus.paymentId\n ),\n `[payment] Successful pre-auth for paymentId=${paymentStatus.paymentId}`\n )\n}\n\n/**\n * Logs failed/cancelled payment\n * @param {PaymentSessionData} session - the session data\n * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay\n */\nfunction logPaymentFailure(session, paymentStatus) {\n logger.info(\n buildPaymentInfo(\n 'pre-auth',\n 'failed/cancelled',\n `${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`,\n session.isLivePayment,\n paymentStatus.paymentId\n ),\n `[payment] Failed/cancelled pre-auth for paymentId=${paymentStatus.paymentId}`\n )\n}\n\n/**\n * Handles successful payment states (capturable/success)\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n * @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay\n */\nfunction handlePaymentSuccess(request, h, session, sessionKey, paymentStatus) {\n flashComponentState(request, session, paymentStatus)\n request.yar.clear(sessionKey)\n return h.redirect(session.returnUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Handles failed/cancelled/error payment states\n * @param {Request} request - the request\n * @param {ResponseToolkit} h - the response toolkit\n * @param {PaymentSessionData} session - the session data\n * @param {string} sessionKey - the session key\n */\nfunction handlePaymentFailure(request, h, session, sessionKey) {\n request.yar.clear(sessionKey)\n return h.redirect(session.failureUrl).code(StatusCodes.SEE_OTHER)\n}\n\n/**\n * Route handler for payment return URL\n * This is called when GOV.UK Pay redirects the user back after payment\n * @returns {ServerRoute}\n */\nfunction getReturnRoute() {\n return {\n method: 'GET',\n path: PAYMENT_RETURN_PATH,\n async handler(request, h) {\n const { uuid } = /** @type {{ uuid: string }} */ (request.query)\n const { session, sessionKey, paymentStatus } = await getPaymentContext(\n request,\n uuid\n )\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const { status } = paymentStatus.state\n\n switch (status) {\n case 'capturable':\n case 'success':\n logPaymentSuccess(session, paymentStatus)\n return handlePaymentSuccess(\n request,\n h,\n session,\n sessionKey,\n paymentStatus\n )\n\n case 'cancelled':\n case 'failed':\n case 'error':\n logPaymentFailure(session, paymentStatus)\n return handlePaymentFailure(request, h, session, sessionKey)\n\n case 'created':\n case 'started':\n case 'submitted': {\n const nextUrl = paymentStatus._links.next_url?.href\n\n if (!nextUrl) {\n throw Boom.badRequest(\n `Payment in state '${status}' but no next_url available`\n )\n }\n\n return h.redirect(nextUrl).code(StatusCodes.SEE_OTHER)\n }\n\n default: {\n const unknownStatus = /** @type {string} */ (status)\n throw Boom.internal(`Unknown payment status: ${unknownStatus}`)\n }\n }\n },\n options: {\n validate: {\n query: Joi.object()\n .keys({\n uuid: Joi.string().uuid().required()\n })\n .required()\n }\n }\n }\n}\n\n/**\n * @import { Request, ResponseToolkit, ServerRoute } from '@hapi/hapi'\n * @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'\n * @import { PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\n * @import { ExternalStateAppendage, FormState } from '~/src/server/plugins/engine/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -9,9 +9,10 @@ export class PaymentService {
|
|
|
9
9
|
* @param {string} description
|
|
10
10
|
* @param {string} returnUrl
|
|
11
11
|
* @param {string} reference
|
|
12
|
+
* @param {boolean} isLivePayment
|
|
12
13
|
* @param {{ formId: string, slug: string }} metadata
|
|
13
14
|
*/
|
|
14
|
-
createPayment(amount: number, description: string, returnUrl: string, reference: string, metadata: {
|
|
15
|
+
createPayment(amount: number, description: string, returnUrl: string, reference: string, isLivePayment: boolean, metadata: {
|
|
15
16
|
formId: string;
|
|
16
17
|
slug: string;
|
|
17
18
|
}): Promise<{
|
|
@@ -20,9 +21,10 @@ export class PaymentService {
|
|
|
20
21
|
}>;
|
|
21
22
|
/**
|
|
22
23
|
* @param {string} paymentId
|
|
24
|
+
* @param {boolean} isLivePayment
|
|
23
25
|
* @returns {Promise<GetPaymentResponse>}
|
|
24
26
|
*/
|
|
25
|
-
getPaymentStatus(paymentId: string): Promise<GetPaymentResponse>;
|
|
27
|
+
getPaymentStatus(paymentId: string, isLivePayment: boolean): Promise<GetPaymentResponse>;
|
|
26
28
|
/**
|
|
27
29
|
* Captures a payment that is in 'capturable' status
|
|
28
30
|
* @param {string} paymentId
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { StatusCodes } from 'http-status-codes';
|
|
2
2
|
import { createLogger } from "../../common/helpers/logging/logger.js";
|
|
3
|
+
import { buildPaymentInfo, convertPenceToPounds } from "../engine/routes/payment-helper.js";
|
|
3
4
|
import { get, post, postJson } from "../../services/httpService.js";
|
|
4
5
|
const PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk';
|
|
5
6
|
const PAYMENT_ENDPOINT = '/v1/payments';
|
|
@@ -31,9 +32,10 @@ export class PaymentService {
|
|
|
31
32
|
* @param {string} description
|
|
32
33
|
* @param {string} returnUrl
|
|
33
34
|
* @param {string} reference
|
|
35
|
+
* @param {boolean} isLivePayment
|
|
34
36
|
* @param {{ formId: string, slug: string }} metadata
|
|
35
37
|
*/
|
|
36
|
-
async createPayment(amount, description, returnUrl, reference, metadata) {
|
|
38
|
+
async createPayment(amount, description, returnUrl, reference, isLivePayment, metadata) {
|
|
37
39
|
const response = await this.postToPayProvider({
|
|
38
40
|
amount,
|
|
39
41
|
description,
|
|
@@ -42,15 +44,7 @@ export class PaymentService {
|
|
|
42
44
|
return_url: returnUrl,
|
|
43
45
|
delayed_capture: true
|
|
44
46
|
});
|
|
45
|
-
logger.info({
|
|
46
|
-
event: {
|
|
47
|
-
category: 'payment',
|
|
48
|
-
action: 'create-payment',
|
|
49
|
-
outcome: 'success',
|
|
50
|
-
reason: `amount=${amount}`,
|
|
51
|
-
reference: response.payment_id
|
|
52
|
-
}
|
|
53
|
-
}, `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`);
|
|
47
|
+
logger.info(buildPaymentInfo('create-payment', 'success', `amount=${convertPenceToPounds(amount)}`, isLivePayment, response.payment_id), `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`);
|
|
54
48
|
return {
|
|
55
49
|
paymentId: response.payment_id,
|
|
56
50
|
paymentUrl: response._links.next_url.href
|
|
@@ -59,9 +53,10 @@ export class PaymentService {
|
|
|
59
53
|
|
|
60
54
|
/**
|
|
61
55
|
* @param {string} paymentId
|
|
56
|
+
* @param {boolean} isLivePayment
|
|
62
57
|
* @returns {Promise<GetPaymentResponse>}
|
|
63
58
|
*/
|
|
64
|
-
async getPaymentStatus(paymentId) {
|
|
59
|
+
async getPaymentStatus(paymentId, isLivePayment) {
|
|
65
60
|
const getByType = /** @type {typeof get<GetPaymentApiResponse>} */get;
|
|
66
61
|
try {
|
|
67
62
|
const response = await getByType(`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`, {
|
|
@@ -73,15 +68,7 @@ export class PaymentService {
|
|
|
73
68
|
throw new Error(`Failed to get payment status: ${errorMessage}`);
|
|
74
69
|
}
|
|
75
70
|
const state = response.payload.state;
|
|
76
|
-
logger.info({
|
|
77
|
-
event: {
|
|
78
|
-
category: 'payment',
|
|
79
|
-
action: 'get-payment-status',
|
|
80
|
-
outcome: state.status === 'capturable' || state.status === 'success' ? 'success' : 'failure',
|
|
81
|
-
reason: `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,
|
|
82
|
-
reference: paymentId
|
|
83
|
-
}
|
|
84
|
-
}, `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`);
|
|
71
|
+
logger.info(buildPaymentInfo('get-payment-status', state.status === 'capturable' || state.status === 'success' ? 'success' : 'failure', `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`, isLivePayment, paymentId), `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`);
|
|
85
72
|
return {
|
|
86
73
|
state,
|
|
87
74
|
_links: response.payload._links,
|
|
@@ -114,7 +101,7 @@ export class PaymentService {
|
|
|
114
101
|
category: 'payment',
|
|
115
102
|
action: 'capture-payment',
|
|
116
103
|
outcome: 'success',
|
|
117
|
-
reason: `amount=${amount}`,
|
|
104
|
+
reason: `amount=${convertPenceToPounds(amount)}`,
|
|
118
105
|
reference: paymentId
|
|
119
106
|
}
|
|
120
107
|
}, `[payment] Successfully captured payment for paymentId=${paymentId}`);
|
|
@@ -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","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":[]}
|
|
1
|
+
{"version":3,"file":"service.js","names":["StatusCodes","createLogger","buildPaymentInfo","convertPenceToPounds","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","isLivePayment","metadata","response","postToPayProvider","return_url","delayed_capture","info","payment_id","paymentId","paymentUrl","_links","next_url","href","getPaymentStatus","getByType","headers","json","error","errorMessage","Error","message","JSON","stringify","state","payload","status","code","email","err","capturePayment","statusCode","res","OK","NO_CONTENT","event","category","action","outcome","reason","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 {\n buildPaymentInfo,\n convertPenceToPounds\n} from '~/src/server/plugins/engine/routes/payment-helper.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {boolean} isLivePayment\n * @param {{ formId: string, slug: string }} metadata\n */\n async createPayment(\n amount,\n description,\n returnUrl,\n reference,\n isLivePayment,\n metadata\n ) {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n buildPaymentInfo(\n 'create-payment',\n 'success',\n `amount=${convertPenceToPounds(amount)}`,\n isLivePayment,\n response.payment_id\n ),\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n }\n\n /**\n * @param {string} paymentId\n * @param {boolean} isLivePayment\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId, isLivePayment) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n buildPaymentInfo(\n 'get-payment-status',\n state.status === 'capturable' || state.status === 'success'\n ? 'success'\n : 'failure',\n `status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,\n isLivePayment,\n paymentId\n ),\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'capture-payment',\n outcome: 'success',\n reason: `amount=${convertPenceToPounds(amount)}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n 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":[]}
|
|
@@ -35,7 +35,7 @@ describe('payment service', () => {
|
|
|
35
35
|
formId: 'form-id',
|
|
36
36
|
slug: 'my-form-slug'
|
|
37
37
|
};
|
|
38
|
-
const payment = await service.createPayment(100, 'Payment description', returnUrl, referenceNumber, metadata);
|
|
38
|
+
const payment = await service.createPayment(100, 'Payment description', returnUrl, referenceNumber, false, metadata);
|
|
39
39
|
expect(payment.paymentId).toBe('payment-id-12345');
|
|
40
40
|
expect(payment.paymentUrl).toBe('http://next-url-href/payment');
|
|
41
41
|
});
|
|
@@ -47,7 +47,7 @@ describe('payment service', () => {
|
|
|
47
47
|
formId: 'form-id',
|
|
48
48
|
slug: 'my-form-slug'
|
|
49
49
|
};
|
|
50
|
-
await expect(() => service.createPayment(100, 'Payment description', returnUrl, referenceNumber, metadata)).rejects.toThrow('internal creation error');
|
|
50
|
+
await expect(() => service.createPayment(100, 'Payment description', returnUrl, referenceNumber, false, metadata)).rejects.toThrow('internal creation error');
|
|
51
51
|
});
|
|
52
52
|
it('should throw if fails to create a payment - bad result from API call', async () => {
|
|
53
53
|
const createPaymentResult = {
|
|
@@ -69,7 +69,7 @@ describe('payment service', () => {
|
|
|
69
69
|
formId: 'form-id',
|
|
70
70
|
slug: 'my-form-slug'
|
|
71
71
|
};
|
|
72
|
-
await expect(() => service.createPayment(100, 'Payment description', returnUrl, referenceNumber, metadata)).rejects.toThrow('Failed to create payment');
|
|
72
|
+
await expect(() => service.createPayment(100, 'Payment description', returnUrl, referenceNumber, false, metadata)).rejects.toThrow('Failed to create payment');
|
|
73
73
|
});
|
|
74
74
|
});
|
|
75
75
|
describe('getPaymentStatus', () => {
|
|
@@ -93,7 +93,7 @@ describe('payment service', () => {
|
|
|
93
93
|
payload: getPaymentStatusResult,
|
|
94
94
|
error: undefined
|
|
95
95
|
});
|
|
96
|
-
const paymentStatus = await service.getPaymentStatus('payment-id-12345');
|
|
96
|
+
const paymentStatus = await service.getPaymentStatus('payment-id-12345', false);
|
|
97
97
|
expect(paymentStatus.paymentId).toBe('payment-id-12345');
|
|
98
98
|
expect(paymentStatus._links.next_url?.href).toBe('http://next-url-href/payment');
|
|
99
99
|
});
|
|
@@ -106,7 +106,7 @@ describe('payment service', () => {
|
|
|
106
106
|
payload: undefined,
|
|
107
107
|
error: new Error('some-error')
|
|
108
108
|
});
|
|
109
|
-
await expect(() => service.getPaymentStatus('payment-id-12345')).rejects.toThrow('Failed to get payment status: some-error');
|
|
109
|
+
await expect(() => service.getPaymentStatus('payment-id-12345', false)).rejects.toThrow('Failed to get payment status: some-error');
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
describe('capturePayment', () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.test.js","names":["PaymentService","get","post","postJson","jest","mock","describe","service","it","expect","toBeDefined","createPaymentResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","undefined","referenceNumber","returnUrl","metadata","formId","slug","payment","createPayment","paymentId","toBe","paymentUrl","mockRejectedValueOnce","Error","rejects","toThrow","getPaymentStatusResult","paymentStatus","getPaymentStatus","capturePaymentResult","captureResult","capturePayment"],"sources":["../../../../src/server/plugins/payment/service.test.js"],"sourcesContent":["import { PaymentService } from '~/src/server/plugins/payment/service.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment service', () => {\n const service = new PaymentService('my-api-key')\n describe('constructor', () => {\n it('should create instance', () => {\n expect(service).toBeDefined()\n })\n })\n\n describe('createPayment', () => {\n it('should create a payment', async () => {\n const createPaymentResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n const payment = await service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n expect(payment.paymentId).toBe('payment-id-12345')\n expect(payment.paymentUrl).toBe('http://next-url-href/payment')\n })\n\n it('should throw if fails to create a payment - failed API call', async () => {\n jest\n .mocked(postJson)\n .mockRejectedValueOnce(new Error('internal creation error'))\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('internal creation error')\n })\n\n it('should throw if fails to create a payment - bad result from API call', async () => {\n const createPaymentResult = {\n state: {\n status: 'failed'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('Failed to create payment')\n })\n })\n\n describe('getPaymentStatus', () => {\n it('should get payment status if exists', async () => {\n const getPaymentStatusResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusResult,\n error: undefined\n })\n\n const paymentStatus = await service.getPaymentStatus('payment-id-12345')\n expect(paymentStatus.paymentId).toBe('payment-id-12345')\n expect(paymentStatus._links.next_url?.href).toBe(\n 'http://next-url-href/payment'\n )\n })\n\n it('should handle payment status error', async () => {\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: undefined,\n error: new Error('some-error')\n })\n\n await expect(() =>\n service.getPaymentStatus('payment-id-12345')\n ).rejects.toThrow('Failed to get payment status: some-error')\n })\n })\n\n describe('capturePayment', () => {\n it('should return true when successful capture with statusCode 200', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return true when successful capture with statusCode 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 204,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return false when status code not 200 or 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 500,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(false)\n })\n\n it('should throw when internal error', async () => {\n jest\n .mocked(post)\n .mockRejectedValueOnce(new Error('internal capture error'))\n\n await expect(() =>\n service.capturePayment('payment-id-12345', 100)\n ).rejects.toThrow('internal capture error')\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"service.test.js","names":["PaymentService","get","post","postJson","jest","mock","describe","service","it","expect","toBeDefined","createPaymentResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","undefined","referenceNumber","returnUrl","metadata","formId","slug","payment","createPayment","paymentId","toBe","paymentUrl","mockRejectedValueOnce","Error","rejects","toThrow","getPaymentStatusResult","paymentStatus","getPaymentStatus","capturePaymentResult","captureResult","capturePayment"],"sources":["../../../../src/server/plugins/payment/service.test.js"],"sourcesContent":["import { PaymentService } from '~/src/server/plugins/payment/service.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment service', () => {\n const service = new PaymentService('my-api-key')\n describe('constructor', () => {\n it('should create instance', () => {\n expect(service).toBeDefined()\n })\n })\n\n describe('createPayment', () => {\n it('should create a payment', async () => {\n const createPaymentResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n const payment = await service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n false,\n metadata\n )\n expect(payment.paymentId).toBe('payment-id-12345')\n expect(payment.paymentUrl).toBe('http://next-url-href/payment')\n })\n\n it('should throw if fails to create a payment - failed API call', async () => {\n jest\n .mocked(postJson)\n .mockRejectedValueOnce(new Error('internal creation error'))\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n false,\n metadata\n )\n ).rejects.toThrow('internal creation error')\n })\n\n it('should throw if fails to create a payment - bad result from API call', async () => {\n const createPaymentResult = {\n state: {\n status: 'failed'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n false,\n metadata\n )\n ).rejects.toThrow('Failed to create payment')\n })\n })\n\n describe('getPaymentStatus', () => {\n it('should get payment status if exists', async () => {\n const getPaymentStatusResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusResult,\n error: undefined\n })\n\n const paymentStatus = await service.getPaymentStatus(\n 'payment-id-12345',\n false\n )\n expect(paymentStatus.paymentId).toBe('payment-id-12345')\n expect(paymentStatus._links.next_url?.href).toBe(\n 'http://next-url-href/payment'\n )\n })\n\n it('should handle payment status error', async () => {\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: undefined,\n error: new Error('some-error')\n })\n\n await expect(() =>\n service.getPaymentStatus('payment-id-12345', false)\n ).rejects.toThrow('Failed to get payment status: some-error')\n })\n })\n\n describe('capturePayment', () => {\n it('should return true when successful capture with statusCode 200', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return true when successful capture with statusCode 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 204,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return false when status code not 200 or 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 500,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(false)\n })\n\n it('should throw when internal error', async () => {\n jest\n .mocked(post)\n .mockRejectedValueOnce(new Error('internal capture error'))\n\n await expect(() =>\n service.capturePayment('payment-id-12345', 100)\n ).rejects.toThrow('internal capture error')\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -225,6 +225,7 @@ export class PaymentField extends FormComponent {
|
|
|
225
225
|
description,
|
|
226
226
|
payCallbackUrl,
|
|
227
227
|
reference,
|
|
228
|
+
isLivePayment,
|
|
228
229
|
{ formId, slug }
|
|
229
230
|
)
|
|
230
231
|
|
|
@@ -276,7 +277,10 @@ export class PaymentField extends FormComponent {
|
|
|
276
277
|
/**
|
|
277
278
|
* @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle
|
|
278
279
|
*/
|
|
279
|
-
const status = await paymentService.getPaymentStatus(
|
|
280
|
+
const status = await paymentService.getPaymentStatus(
|
|
281
|
+
paymentId,
|
|
282
|
+
isLivePayment
|
|
283
|
+
)
|
|
280
284
|
|
|
281
285
|
PaymentSubmissionError.checkPaymentAmount(
|
|
282
286
|
status.amount,
|
|
@@ -28,11 +28,48 @@ export async function getPaymentContext(request, uuid) {
|
|
|
28
28
|
|
|
29
29
|
const apiKey = getPaymentApiKey(isLivePayment, formId)
|
|
30
30
|
const paymentService = new PaymentService(apiKey)
|
|
31
|
-
const paymentStatus = await paymentService.getPaymentStatus(
|
|
31
|
+
const paymentStatus = await paymentService.getPaymentStatus(
|
|
32
|
+
paymentId,
|
|
33
|
+
isLivePayment
|
|
34
|
+
)
|
|
32
35
|
|
|
33
36
|
return { session, sessionKey, paymentStatus }
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Builds an object for logging payment information
|
|
41
|
+
* @param {string} action
|
|
42
|
+
* @param {string} outcome
|
|
43
|
+
* @param {string} reason
|
|
44
|
+
* @param {boolean} isLivePayment
|
|
45
|
+
* @param {string} paymentId
|
|
46
|
+
*/
|
|
47
|
+
export function buildPaymentInfo(
|
|
48
|
+
action,
|
|
49
|
+
outcome,
|
|
50
|
+
reason,
|
|
51
|
+
isLivePayment,
|
|
52
|
+
paymentId
|
|
53
|
+
) {
|
|
54
|
+
return {
|
|
55
|
+
event: {
|
|
56
|
+
category: 'payment',
|
|
57
|
+
action,
|
|
58
|
+
outcome,
|
|
59
|
+
reason,
|
|
60
|
+
type: isLivePayment ? 'live' : 'test',
|
|
61
|
+
reference: paymentId
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {number} amount
|
|
68
|
+
*/
|
|
69
|
+
export function convertPenceToPounds(amount) {
|
|
70
|
+
return `${amount / 100}`
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
/**
|
|
37
74
|
* @import { Request } from '@hapi/hapi'
|
|
38
75
|
* @import { GetPaymentResponse, PaymentSessionData } from '~/src/server/plugins/payment/types.js'
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
buildPaymentInfo,
|
|
3
|
+
getPaymentContext
|
|
4
|
+
} from '~/src/server/plugins/engine/routes/payment-helper.js'
|
|
2
5
|
import { get } from '~/src/server/services/httpService.js'
|
|
3
6
|
|
|
4
7
|
jest.mock('~/src/server/services/httpService.ts')
|
|
@@ -83,6 +86,46 @@ describe('payment helper', () => {
|
|
|
83
86
|
sessionKey: 'payment-5a54c2fe-da49-4202-8cd3-2121eaca03c3'
|
|
84
87
|
})
|
|
85
88
|
})
|
|
89
|
+
|
|
90
|
+
it('should create logging info for a test payment', () => {
|
|
91
|
+
const res = buildPaymentInfo(
|
|
92
|
+
'action1',
|
|
93
|
+
'outcome1',
|
|
94
|
+
'reason1',
|
|
95
|
+
false,
|
|
96
|
+
'pay-123'
|
|
97
|
+
)
|
|
98
|
+
expect(res).toEqual({
|
|
99
|
+
event: {
|
|
100
|
+
category: 'payment',
|
|
101
|
+
action: 'action1',
|
|
102
|
+
outcome: 'outcome1',
|
|
103
|
+
reason: 'reason1',
|
|
104
|
+
type: 'test',
|
|
105
|
+
reference: 'pay-123'
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should create logging info for a live payment', () => {
|
|
111
|
+
const res = buildPaymentInfo(
|
|
112
|
+
'action2',
|
|
113
|
+
'outcome2',
|
|
114
|
+
'reason2',
|
|
115
|
+
true,
|
|
116
|
+
'pay-123'
|
|
117
|
+
)
|
|
118
|
+
expect(res).toEqual({
|
|
119
|
+
event: {
|
|
120
|
+
category: 'payment',
|
|
121
|
+
action: 'action2',
|
|
122
|
+
outcome: 'outcome2',
|
|
123
|
+
reason: 'reason2',
|
|
124
|
+
type: 'live',
|
|
125
|
+
reference: 'pay-123'
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
})
|
|
86
129
|
})
|
|
87
130
|
|
|
88
131
|
/**
|
|
@@ -2,12 +2,19 @@ import Boom from '@hapi/boom'
|
|
|
2
2
|
import { StatusCodes } from 'http-status-codes'
|
|
3
3
|
import Joi from 'joi'
|
|
4
4
|
|
|
5
|
+
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
5
6
|
import { EXTERNAL_STATE_APPENDAGE } from '~/src/server/constants.js'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildPaymentInfo,
|
|
9
|
+
convertPenceToPounds,
|
|
10
|
+
getPaymentContext
|
|
11
|
+
} from '~/src/server/plugins/engine/routes/payment-helper.js'
|
|
7
12
|
|
|
8
13
|
export const PAYMENT_RETURN_PATH = '/payment-callback'
|
|
9
14
|
export const PAYMENT_SESSION_PREFIX = 'payment-'
|
|
10
15
|
|
|
16
|
+
const logger = createLogger()
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Flash form component state after successful payment
|
|
13
20
|
* @param {Request} request - the request
|
|
@@ -48,6 +55,42 @@ export function getRoutes() {
|
|
|
48
55
|
return [getReturnRoute()]
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Logs successful payment
|
|
60
|
+
* @param {PaymentSessionData} session - the session data
|
|
61
|
+
* @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay
|
|
62
|
+
*/
|
|
63
|
+
function logPaymentSuccess(session, paymentStatus) {
|
|
64
|
+
logger.info(
|
|
65
|
+
buildPaymentInfo(
|
|
66
|
+
'pre-auth',
|
|
67
|
+
'success',
|
|
68
|
+
`${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`,
|
|
69
|
+
session.isLivePayment,
|
|
70
|
+
paymentStatus.paymentId
|
|
71
|
+
),
|
|
72
|
+
`[payment] Successful pre-auth for paymentId=${paymentStatus.paymentId}`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Logs failed/cancelled payment
|
|
78
|
+
* @param {PaymentSessionData} session - the session data
|
|
79
|
+
* @param {GetPaymentResponse} paymentStatus - the payment status from GOV.UK Pay
|
|
80
|
+
*/
|
|
81
|
+
function logPaymentFailure(session, paymentStatus) {
|
|
82
|
+
logger.info(
|
|
83
|
+
buildPaymentInfo(
|
|
84
|
+
'pre-auth',
|
|
85
|
+
'failed/cancelled',
|
|
86
|
+
`${paymentStatus.state.status} amount=${convertPenceToPounds(paymentStatus.amount)}`,
|
|
87
|
+
session.isLivePayment,
|
|
88
|
+
paymentStatus.paymentId
|
|
89
|
+
),
|
|
90
|
+
`[payment] Failed/cancelled pre-auth for paymentId=${paymentStatus.paymentId}`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
51
94
|
/**
|
|
52
95
|
* Handles successful payment states (capturable/success)
|
|
53
96
|
* @param {Request} request - the request
|
|
@@ -98,6 +141,7 @@ function getReturnRoute() {
|
|
|
98
141
|
switch (status) {
|
|
99
142
|
case 'capturable':
|
|
100
143
|
case 'success':
|
|
144
|
+
logPaymentSuccess(session, paymentStatus)
|
|
101
145
|
return handlePaymentSuccess(
|
|
102
146
|
request,
|
|
103
147
|
h,
|
|
@@ -109,6 +153,7 @@ function getReturnRoute() {
|
|
|
109
153
|
case 'cancelled':
|
|
110
154
|
case 'failed':
|
|
111
155
|
case 'error':
|
|
156
|
+
logPaymentFailure(session, paymentStatus)
|
|
112
157
|
return handlePaymentFailure(request, h, session, sessionKey)
|
|
113
158
|
|
|
114
159
|
case 'created':
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { StatusCodes } from 'http-status-codes'
|
|
2
2
|
|
|
3
3
|
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
4
|
+
import {
|
|
5
|
+
buildPaymentInfo,
|
|
6
|
+
convertPenceToPounds
|
|
7
|
+
} from '~/src/server/plugins/engine/routes/payment-helper.js'
|
|
4
8
|
import { get, post, postJson } from '~/src/server/services/httpService.js'
|
|
5
9
|
|
|
6
10
|
const PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'
|
|
@@ -35,9 +39,17 @@ export class PaymentService {
|
|
|
35
39
|
* @param {string} description
|
|
36
40
|
* @param {string} returnUrl
|
|
37
41
|
* @param {string} reference
|
|
42
|
+
* @param {boolean} isLivePayment
|
|
38
43
|
* @param {{ formId: string, slug: string }} metadata
|
|
39
44
|
*/
|
|
40
|
-
async createPayment(
|
|
45
|
+
async createPayment(
|
|
46
|
+
amount,
|
|
47
|
+
description,
|
|
48
|
+
returnUrl,
|
|
49
|
+
reference,
|
|
50
|
+
isLivePayment,
|
|
51
|
+
metadata
|
|
52
|
+
) {
|
|
41
53
|
const response = await this.postToPayProvider({
|
|
42
54
|
amount,
|
|
43
55
|
description,
|
|
@@ -48,15 +60,13 @@ export class PaymentService {
|
|
|
48
60
|
})
|
|
49
61
|
|
|
50
62
|
logger.info(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
},
|
|
63
|
+
buildPaymentInfo(
|
|
64
|
+
'create-payment',
|
|
65
|
+
'success',
|
|
66
|
+
`amount=${convertPenceToPounds(amount)}`,
|
|
67
|
+
isLivePayment,
|
|
68
|
+
response.payment_id
|
|
69
|
+
),
|
|
60
70
|
`[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`
|
|
61
71
|
)
|
|
62
72
|
|
|
@@ -68,9 +78,10 @@ export class PaymentService {
|
|
|
68
78
|
|
|
69
79
|
/**
|
|
70
80
|
* @param {string} paymentId
|
|
81
|
+
* @param {boolean} isLivePayment
|
|
71
82
|
* @returns {Promise<GetPaymentResponse>}
|
|
72
83
|
*/
|
|
73
|
-
async getPaymentStatus(paymentId) {
|
|
84
|
+
async getPaymentStatus(paymentId, isLivePayment) {
|
|
74
85
|
const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)
|
|
75
86
|
|
|
76
87
|
try {
|
|
@@ -92,18 +103,15 @@ export class PaymentService {
|
|
|
92
103
|
|
|
93
104
|
const state = response.payload.state
|
|
94
105
|
logger.info(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
reference: paymentId
|
|
105
|
-
}
|
|
106
|
-
},
|
|
106
|
+
buildPaymentInfo(
|
|
107
|
+
'get-payment-status',
|
|
108
|
+
state.status === 'capturable' || state.status === 'success'
|
|
109
|
+
? 'success'
|
|
110
|
+
: 'failure',
|
|
111
|
+
`status:${state.status} code:${state.code ?? 'N/A'} message:${state.message ?? 'N/A'}`,
|
|
112
|
+
isLivePayment,
|
|
113
|
+
paymentId
|
|
114
|
+
),
|
|
107
115
|
`[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`
|
|
108
116
|
)
|
|
109
117
|
|
|
@@ -151,7 +159,7 @@ export class PaymentService {
|
|
|
151
159
|
category: 'payment',
|
|
152
160
|
action: 'capture-payment',
|
|
153
161
|
outcome: 'success',
|
|
154
|
-
reason: `amount=${amount}`,
|
|
162
|
+
reason: `amount=${convertPenceToPounds(amount)}`,
|
|
155
163
|
reference: paymentId
|
|
156
164
|
}
|
|
157
165
|
},
|
|
@@ -41,6 +41,7 @@ describe('payment service', () => {
|
|
|
41
41
|
'Payment description',
|
|
42
42
|
returnUrl,
|
|
43
43
|
referenceNumber,
|
|
44
|
+
false,
|
|
44
45
|
metadata
|
|
45
46
|
)
|
|
46
47
|
expect(payment.paymentId).toBe('payment-id-12345')
|
|
@@ -61,6 +62,7 @@ describe('payment service', () => {
|
|
|
61
62
|
'Payment description',
|
|
62
63
|
returnUrl,
|
|
63
64
|
referenceNumber,
|
|
65
|
+
false,
|
|
64
66
|
metadata
|
|
65
67
|
)
|
|
66
68
|
).rejects.toThrow('internal creation error')
|
|
@@ -90,6 +92,7 @@ describe('payment service', () => {
|
|
|
90
92
|
'Payment description',
|
|
91
93
|
returnUrl,
|
|
92
94
|
referenceNumber,
|
|
95
|
+
false,
|
|
93
96
|
metadata
|
|
94
97
|
)
|
|
95
98
|
).rejects.toThrow('Failed to create payment')
|
|
@@ -119,7 +122,10 @@ describe('payment service', () => {
|
|
|
119
122
|
error: undefined
|
|
120
123
|
})
|
|
121
124
|
|
|
122
|
-
const paymentStatus = await service.getPaymentStatus(
|
|
125
|
+
const paymentStatus = await service.getPaymentStatus(
|
|
126
|
+
'payment-id-12345',
|
|
127
|
+
false
|
|
128
|
+
)
|
|
123
129
|
expect(paymentStatus.paymentId).toBe('payment-id-12345')
|
|
124
130
|
expect(paymentStatus._links.next_url?.href).toBe(
|
|
125
131
|
'http://next-url-href/payment'
|
|
@@ -137,7 +143,7 @@ describe('payment service', () => {
|
|
|
137
143
|
})
|
|
138
144
|
|
|
139
145
|
await expect(() =>
|
|
140
|
-
service.getPaymentStatus('payment-id-12345')
|
|
146
|
+
service.getPaymentStatus('payment-id-12345', false)
|
|
141
147
|
).rejects.toThrow('Failed to get payment status: some-error')
|
|
142
148
|
})
|
|
143
149
|
})
|