@defra/forms-engine-plugin 4.0.52 → 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/pageControllers/FileUploadPageController.js +14 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.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/pageControllers/FileUploadPageController.test.ts +84 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +20 -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":"AAAA,SAASA,UAAU,QAAQ,aAAa;AAMxC,SAASC,WAAW,QAAQ,mBAAmB;AAC/C,OAAOC,GAAG,MAA6B,KAAK;AAE5C,SAASC,aAAa;AAEtB,SAASC,gBAAgB;AACzB,SACEC,iBAAiB,EACjBC,mBAAmB,EACnBC,sBAAsB;AAgBxB,SACEC,oBAAoB,EACpBC,cAAc;AAGhB,OAAO,MAAMC,YAAY,SAASP,aAAa,CAAC;EAI9CQ,4BAA4B,GAAG,IAAI;EAEnCC,WAAWA,CACTC,GAA0B,EAC1BC,KAAqD,EACrD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,IAAI,CAACC,OAAO,GAAGF,GAAG,CAACE,OAAO;IAE1B,MAAMC,kBAAkB,GAAGd,GAAG,CAC3Be,MAAM,CAAC;MACNC,SAAS,EAAEhB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCC,SAAS,EAAEnB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCE,MAAM,EAAEpB,GAAG,CAACqB,MAAM,CAAC,CAAC,CAACH,QAAQ,CAAC,CAAC;MAC/BI,WAAW,EAAEtB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MACpCK,IAAI,EAAEvB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACM,IAAI,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;MACpCM,MAAM,EAAExB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAC/BO,aAAa,EAAEzB,GAAG,CAAC0B,OAAO,CAAC,CAAC,CAACR,QAAQ,CAAC,CAAC;MACvCS,OAAO,EAAE3B,GAAG,CACTe,MAAM,CAAC;QACNa,MAAM,EAAE5B,GAAG,CACRiB,MAAM,CAAC,CAAC,CACRY,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CACrCX,QAAQ,CAAC,CAAC;QACbY,SAAS,EAAE9B,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACc,OAAO,CAAC,CAAC,CAACb,QAAQ,CAAC;MAC7C,CAAC,CAAC,CACDA,QAAQ,CAAC;IACd,CAAC,CAAC,CACDc,OAAO,CAAC,IAAI,CAAC,CACbC,KAAK,CAAC,IAAI,CAACA,KAAK,CAAC;IAEpB,IAAI,CAACC,UAAU,GAAGpB,kBAAkB;IACpC;IACA;IACA;IACA,IAAI,CAACqB,WAAW,GAAGrB,kBAAkB,CAACI,QAAQ,CAAC,CAAC;EAClD;;EAEA;AACF;AACA;EACEkB,wBAAwBA,CACtBC,KAA0B,EACA;IAC1B,MAAMC,KAAK,GAAGD,KAAK,CAAC,IAAI,CAACE,IAAI,CAAC;IAC9B,OAAO,IAAI,CAACC,cAAc,CAACF,KAAK,CAAC,GAAGA,KAAK,GAAGG,SAAS;EACvD;EAEAC,yBAAyBA,CAACL,KAA0B,EAAU;IAC5D,MAAMC,KAAK,GAAG,IAAI,CAACF,wBAAwB,CAACC,KAAK,CAAC;IAElD,IAAI,CAACC,KAAK,EAAE;MACV,OAAO,EAAE;IACX;IAEA,OAAO,GAAG/B,cAAc,CAAC+B,KAAK,CAAClB,MAAM,CAAC,MAAMkB,KAAK,CAAChB,WAAW,EAAE;EACjE;EAEAqB,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAMC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;;IAErD;IACA,MAAME,YAAY,GAAG,IAAI,CAACP,cAAc,CAACI,OAAO,CAAC,IAAI,CAACL,IAAI,CAAY,CAAC,GAClEK,OAAO,CAAC,IAAI,CAACL,IAAI,CAAC,GACnBE,SAAS;;IAEb;IACA,MAAMrB,MAAM,GAAG2B,YAAY,EAAE3B,MAAM,IAAI,IAAI,CAACP,OAAO,CAACO,MAAM;IAE1D,OAAO;MACL,GAAG0B,SAAS;MACZ1B,MAAM,EAAEb,cAAc,CAACa,MAAM,CAAC;MAC9BE,WAAW,EAAE,IAAI,CAACT,OAAO,CAACS,WAAW;MACrCyB;IACF,CAAC;EACH;;EAEA;AACF;AACA;EACEP,cAAcA,CAACF,KAAc,EAAyB;IACpD,OAAO9B,YAAY,CAACgC,cAAc,CAACF,KAAK,CAAC;EAC3C;;EAEA;AACF;AACA;EACE,OAAOE,cAAcA,CAACF,KAAc,EAAyB;IAC3D,IAAI,CAACA,KAAK,IAAI,OAAOA,KAAK,KAAK,QAAQ,IAAIU,KAAK,CAACC,OAAO,CAACX,KAAK,CAAC,EAAE;MAC/D,OAAO,KAAK;IACd;IAEA,MAAMD,KAAK,GAAGC,KAAqB;IACnC,OACE,OAAOD,KAAK,CAACrB,SAAS,KAAK,QAAQ,IACnC,OAAOqB,KAAK,CAACjB,MAAM,KAAK,QAAQ,IAChC,OAAOiB,KAAK,CAACf,WAAW,KAAK,QAAQ;EAEzC;;EAEA;AACF;AACA;EACE4B,OAAOA,CAACZ,KAAkC,EAAsB;IAC9D,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC;EACnC;EAEAa,YAAYA,CAACb,KAAkC,EAAE;IAC/C,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC,GAC5BA,KAAK,GACNG,SAAS;EACf;EAEAW,wBAAwBA,CAACf,KAA0B,EAAE;IACnD,OAAO,IAAI,CAACG,cAAc,CAACH,KAAK,CAAC,GAC7B,cAAcA,KAAK,CAAClB,SAAS,aAAaZ,cAAc,CAAC8B,KAAK,CAACjB,MAAM,CAAC,EAAE,GACxE,EAAE;EACR;;EAEA;AACF;AACA;EACEiC,oBAAoBA,CAAA,EAA6B;IAC/C,OAAO7C,YAAY,CAAC6C,oBAAoB,CAAC,CAAC;EAC5C;;EAEA;AACF;AACA;EACE,OAAOA,oBAAoBA,CAAA,EAA6B;IACtD,OAAO;MACLC,UAAU,EAAE,CACV;QACEC,IAAI,EAAE,iBAAiB;QACvBC,QAAQ,EAAE;MACZ,CAAC,CACF;MACDC,sBAAsB,EAAE;IAC1B,CAAC;EACH;;EAEA;AACF;AACA;EACE,aAAaC,UAAUA,CACrBC,OAA2B,EAC3BC,CAAsB,EACtBC,IAA2B,EACT;IAClB,MAAM;MAAEhD,OAAO;MAAE0B,IAAI,EAAEuB;IAAc,CAAC,GAAGD,IAAI,CAACE,SAAS;IACvD,MAAM;MAAEC;IAAM,CAAC,GAAGH,IAAI,CAACI,UAAU;IAEjC,MAAM5B,KAAK,GAAG,MAAMwB,IAAI,CAACI,UAAU,CAACC,QAAQ,CAACP,OAAO,CAAC;IACrD,MAAM;MAAEQ;IAAQ,CAAC,GAAGjE,gBAAgB,CAACyD,OAAO,CAACS,MAAM,CAAC;IACpD,MAAMC,UAAU,GAAG,GAAGF,OAAO,IAAIH,KAAK,CAACM,QAAQ,UAAU;IAEzD,MAAMC,oBAAoB,GAAGlC,KAAK,CAACyB,aAAa,CAAC;IACjD,IACEtD,YAAY,CAACgC,cAAc,CAAC+B,oBAAoB,CAAC,IACjDA,oBAAoB,CAAC5C,OAAO,EAAEC,MAAM,KAAK,SAAS,EAClD;MACA,OAAOgC,CAAC,CAACY,QAAQ,CAACH,UAAU,CAAC,CAACI,IAAI,CAAC1E,WAAW,CAAC2E,SAAS,CAAC;IAC3D;IAEA,MAAMjD,aAAa,GAAGoC,IAAI,CAACc,MAAM,IAAI,CAACd,IAAI,CAACe,SAAS;IACpD,MAAMpD,MAAM,GAAGqC,IAAI,CAACI,UAAU,CAACD,KAAK,CAACxC,MAAM;IAC3C,MAAMqD,cAAc,GAAGvE,oBAAoB,CAACmB,aAAa,EAAED,MAAM,CAAC;IAElE,MAAMD,IAAI,GAAGzB,UAAU,CAAC,CAAC;IAEzB,MAAMqB,SAAS,GAAGkB,KAAK,CAACyC,mBAA6B;IACrD,MAAM1D,MAAM,GAAGP,OAAO,CAACO,MAAM;IAE7B,MAAME,WAAW,GAAGT,OAAO,CAACS,WAAW;IAEvC,MAAMyD,IAAI,GAAG,IAAIf,KAAK,CAACM,QAAQ,EAAE;IAEjC,MAAMU,cAAc,GAAG,GAAGb,OAAO,0BAA0B5C,IAAI,EAAE;IACjE,MAAM0D,cAAc,GAAGpB,IAAI,CAACqB,SAAS;IAErC,MAAMC,aAAa,GAAGC,IAAI,CAACC,KAAK,CAACjE,MAAM,GAAG,GAAG,CAAC;IAC9C,MAAMkE,OAAO,GAAG,MAAMT,cAAc,CAACU,aAAa,CAChDJ,aAAa,EACb7D,WAAW,EACX0D,cAAc,EACd7D,SAAS,EACT;MAAEK,MAAM;MAAEuD;IAAK,CACjB,CAAC;IAED,MAAMS,WAA+B,GAAG;MACtCjE,IAAI;MACJC,MAAM;MACNL,SAAS;MACTC,MAAM;MACNE,WAAW;MACXN,SAAS,EAAEsE,OAAO,CAACtE,SAAS;MAC5B8C,aAAa;MACb2B,SAAS,EAAEpB,UAAU;MACrBqB,UAAU,EAAET,cAAc;MAC1BxD;IACF,CAAC;IAEDkC,OAAO,CAACgC,GAAG,CAACC,GAAG,CAAC,WAAWrE,IAAI,EAAE,EAAEiE,WAAW,CAAC;IAE/C,OAAO5B,CAAC,CAACY,QAAQ,CAACc,OAAO,CAACO,UAAU,CAAC,CAACpB,IAAI,CAAC1E,WAAW,CAAC2E,SAAS,CAAC;EACnE;;EAEA;AACF;AACA;AACA;EACE,MAAMoB,QAAQA,CACZnC,OAA2B,EAC3BoC,SAAuB,EACvBC,OAAoB,EACL;IACf,MAAMjD,YAAY,GAAG,IAAI,CAACX,wBAAwB,CAAC4D,OAAO,CAAC3D,KAAK,CAAC;IAEjE,IAAI,CAACU,YAAY,EAAE;MACjB,MAAM,IAAI3C,mBAAmB,CAC3B,IAAI,EACJ,kCAAkC,EAClC,IAAI,EACJD,iBAAiB,CAAC8F,iBACpB,CAAC;IACH;IAEA,IAAIlD,YAAY,CAACmD,OAAO,EAAEtE,MAAM,KAAK,SAAS,EAAE;MAC9C;IACF;IAEA,MAAM;MAAEZ,SAAS;MAAES,aAAa;MAAED;IAAO,CAAC,GAAGuB,YAAY;IACzD,MAAM8B,cAAc,GAAGvE,oBAAoB,CAACmB,aAAa,EAAED,MAAM,CAAC;;IAElE;AACJ;AACA;IACI,MAAMI,MAAM,GAAG,MAAMiD,cAAc,CAACsB,gBAAgB,CAACnF,SAAS,CAAC;IAE/DX,sBAAsB,CAAC+F,kBAAkB,CACvCxE,MAAM,CAACR,MAAM,EACb,IAAI,CAACP,OAAO,CAACO,MAAM,EACnB,IACF,CAAC;IAED,IAAIQ,MAAM,CAACS,KAAK,CAACT,MAAM,KAAK,SAAS,EAAE;MACrC,MAAM,IAAI,CAACyE,mBAAmB,CAAC1C,OAAO,EAAEZ,YAAY,CAAC;MACrD;IACF;IAEA,IAAInB,MAAM,CAACS,KAAK,CAACT,MAAM,KAAK,YAAY,EAAE;MACxC,MAAM,IAAIxB,mBAAmB,CAC3B,IAAI,EACJ,gFAAgF,EAChF,IAAI,EACJD,iBAAiB,CAACmG,cACpB,CAAC;IACH;IAEA,MAAMC,QAAQ,GAAG,MAAM1B,cAAc,CAAC2B,cAAc,CAClDxF,SAAS,EACTY,MAAM,CAACR,MACT,CAAC;IAED,IAAI,CAACmF,QAAQ,EAAE;MACb,MAAM,IAAInG,mBAAmB,CAC3B,IAAI,EACJ,qFAAqF,EACrF,KACF,CAAC;IACH;IAEA,MAAM,IAAI,CAACiG,mBAAmB,CAAC1C,OAAO,EAAEZ,YAAY,CAAC;EACvD;;EAEA;AACF;AACA;AACA;EACE,MAAcsD,mBAAmBA,CAC/B1C,OAA2B,EAC3BZ,YAA0B,EACX;IACf,MAAM0D,YAA0B,GAAG;MACjC,GAAG1D,YAAY;MACfmD,OAAO,EAAE;QACPtE,MAAM,EAAE,SAAS;QACjBE,SAAS,EAAE,IAAI4E,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC;MACpC;IACF,CAAC;IAED,IAAI,IAAI,CAACC,IAAI,EAAE;MACb,MAAMC,YAAY,GAAG,MAAM,IAAI,CAACD,IAAI,CAAC1C,QAAQ,CAACP,OAAO,CAAC;MACtD,MAAM,IAAI,CAACiD,IAAI,CAACE,UAAU,CAACnD,OAAO,EAAEkD,YAAY,EAAE;QAChD,CAAC,IAAI,CAACtE,IAAI,GAAGkE;MACf,CAAC,CAAC;IACJ;EACF;AACF;;AAiBA;AACA;AACA","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":"AAAA,SAASA,UAAU,QAAQ,aAAa;AAMxC,SAASC,WAAW,QAAQ,mBAAmB;AAC/C,OAAOC,GAAG,MAA6B,KAAK;AAE5C,SAASC,aAAa;AAEtB,SAASC,gBAAgB;AACzB,SACEC,iBAAiB,EACjBC,mBAAmB,EACnBC,sBAAsB;AAgBxB,SACEC,oBAAoB,EACpBC,cAAc;AAGhB,OAAO,MAAMC,YAAY,SAASP,aAAa,CAAC;EAI9CQ,4BAA4B,GAAG,IAAI;EAEnCC,WAAWA,CACTC,GAA0B,EAC1BC,KAAqD,EACrD;IACA,KAAK,CAACD,GAAG,EAAEC,KAAK,CAAC;IAEjB,IAAI,CAACC,OAAO,GAAGF,GAAG,CAACE,OAAO;IAE1B,MAAMC,kBAAkB,GAAGd,GAAG,CAC3Be,MAAM,CAAC;MACNC,SAAS,EAAEhB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCC,SAAS,EAAEnB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCE,MAAM,EAAEpB,GAAG,CAACqB,MAAM,CAAC,CAAC,CAACH,QAAQ,CAAC,CAAC;MAC/BI,WAAW,EAAEtB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MACpCK,IAAI,EAAEvB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACM,IAAI,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;MACpCM,MAAM,EAAExB,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAC/BO,aAAa,EAAEzB,GAAG,CAAC0B,OAAO,CAAC,CAAC,CAACR,QAAQ,CAAC,CAAC;MACvCS,OAAO,EAAE3B,GAAG,CACTe,MAAM,CAAC;QACNa,MAAM,EAAE5B,GAAG,CACRiB,MAAM,CAAC,CAAC,CACRY,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CACrCX,QAAQ,CAAC,CAAC;QACbY,SAAS,EAAE9B,GAAG,CAACiB,MAAM,CAAC,CAAC,CAACc,OAAO,CAAC,CAAC,CAACb,QAAQ,CAAC;MAC7C,CAAC,CAAC,CACDA,QAAQ,CAAC;IACd,CAAC,CAAC,CACDc,OAAO,CAAC,IAAI,CAAC,CACbC,KAAK,CAAC,IAAI,CAACA,KAAK,CAAC;IAEpB,IAAI,CAACC,UAAU,GAAGpB,kBAAkB;IACpC;IACA;IACA;IACA,IAAI,CAACqB,WAAW,GAAGrB,kBAAkB,CAACI,QAAQ,CAAC,CAAC;EAClD;;EAEA;AACF;AACA;EACEkB,wBAAwBA,CACtBC,KAA0B,EACA;IAC1B,MAAMC,KAAK,GAAGD,KAAK,CAAC,IAAI,CAACE,IAAI,CAAC;IAC9B,OAAO,IAAI,CAACC,cAAc,CAACF,KAAK,CAAC,GAAGA,KAAK,GAAGG,SAAS;EACvD;EAEAC,yBAAyBA,CAACL,KAA0B,EAAU;IAC5D,MAAMC,KAAK,GAAG,IAAI,CAACF,wBAAwB,CAACC,KAAK,CAAC;IAElD,IAAI,CAACC,KAAK,EAAE;MACV,OAAO,EAAE;IACX;IAEA,OAAO,GAAG/B,cAAc,CAAC+B,KAAK,CAAClB,MAAM,CAAC,MAAMkB,KAAK,CAAChB,WAAW,EAAE;EACjE;EAEAqB,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAMC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;;IAErD;IACA,MAAME,YAAY,GAAG,IAAI,CAACP,cAAc,CAACI,OAAO,CAAC,IAAI,CAACL,IAAI,CAAY,CAAC,GAClEK,OAAO,CAAC,IAAI,CAACL,IAAI,CAAC,GACnBE,SAAS;;IAEb;IACA,MAAMrB,MAAM,GAAG2B,YAAY,EAAE3B,MAAM,IAAI,IAAI,CAACP,OAAO,CAACO,MAAM;IAE1D,OAAO;MACL,GAAG0B,SAAS;MACZ1B,MAAM,EAAEb,cAAc,CAACa,MAAM,CAAC;MAC9BE,WAAW,EAAE,IAAI,CAACT,OAAO,CAACS,WAAW;MACrCyB;IACF,CAAC;EACH;;EAEA;AACF;AACA;EACEP,cAAcA,CAACF,KAAc,EAAyB;IACpD,OAAO9B,YAAY,CAACgC,cAAc,CAACF,KAAK,CAAC;EAC3C;;EAEA;AACF;AACA;EACE,OAAOE,cAAcA,CAACF,KAAc,EAAyB;IAC3D,IAAI,CAACA,KAAK,IAAI,OAAOA,KAAK,KAAK,QAAQ,IAAIU,KAAK,CAACC,OAAO,CAACX,KAAK,CAAC,EAAE;MAC/D,OAAO,KAAK;IACd;IAEA,MAAMD,KAAK,GAAGC,KAAqB;IACnC,OACE,OAAOD,KAAK,CAACrB,SAAS,KAAK,QAAQ,IACnC,OAAOqB,KAAK,CAACjB,MAAM,KAAK,QAAQ,IAChC,OAAOiB,KAAK,CAACf,WAAW,KAAK,QAAQ;EAEzC;;EAEA;AACF;AACA;EACE4B,OAAOA,CAACZ,KAAkC,EAAsB;IAC9D,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC;EACnC;EAEAa,YAAYA,CAACb,KAAkC,EAAE;IAC/C,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC,GAC5BA,KAAK,GACNG,SAAS;EACf;EAEAW,wBAAwBA,CAACf,KAA0B,EAAE;IACnD,OAAO,IAAI,CAACG,cAAc,CAACH,KAAK,CAAC,GAC7B,cAAcA,KAAK,CAAClB,SAAS,aAAaZ,cAAc,CAAC8B,KAAK,CAACjB,MAAM,CAAC,EAAE,GACxE,EAAE;EACR;;EAEA;AACF;AACA;EACEiC,oBAAoBA,CAAA,EAA6B;IAC/C,OAAO7C,YAAY,CAAC6C,oBAAoB,CAAC,CAAC;EAC5C;;EAEA;AACF;AACA;EACE,OAAOA,oBAAoBA,CAAA,EAA6B;IACtD,OAAO;MACLC,UAAU,EAAE,CACV;QACEC,IAAI,EAAE,iBAAiB;QACvBC,QAAQ,EAAE;MACZ,CAAC,CACF;MACDC,sBAAsB,EAAE;IAC1B,CAAC;EACH;;EAEA;AACF;AACA;EACE,aAAaC,UAAUA,CACrBC,OAA2B,EAC3BC,CAAsB,EACtBC,IAA2B,EACT;IAClB,MAAM;MAAEhD,OAAO;MAAE0B,IAAI,EAAEuB;IAAc,CAAC,GAAGD,IAAI,CAACE,SAAS;IACvD,MAAM;MAAEC;IAAM,CAAC,GAAGH,IAAI,CAACI,UAAU;IAEjC,MAAM5B,KAAK,GAAG,MAAMwB,IAAI,CAACI,UAAU,CAACC,QAAQ,CAACP,OAAO,CAAC;IACrD,MAAM;MAAEQ;IAAQ,CAAC,GAAGjE,gBAAgB,CAACyD,OAAO,CAACS,MAAM,CAAC;IACpD,MAAMC,UAAU,GAAG,GAAGF,OAAO,IAAIH,KAAK,CAACM,QAAQ,UAAU;IAEzD,MAAMC,oBAAoB,GAAGlC,KAAK,CAACyB,aAAa,CAAC;IACjD,IACEtD,YAAY,CAACgC,cAAc,CAAC+B,oBAAoB,CAAC,IACjDA,oBAAoB,CAAC5C,OAAO,EAAEC,MAAM,KAAK,SAAS,EAClD;MACA,OAAOgC,CAAC,CAACY,QAAQ,CAACH,UAAU,CAAC,CAACI,IAAI,CAAC1E,WAAW,CAAC2E,SAAS,CAAC;IAC3D;IAEA,MAAMjD,aAAa,GAAGoC,IAAI,CAACc,MAAM,IAAI,CAACd,IAAI,CAACe,SAAS;IACpD,MAAMpD,MAAM,GAAGqC,IAAI,CAACI,UAAU,CAACD,KAAK,CAACxC,MAAM;IAC3C,MAAMqD,cAAc,GAAGvE,oBAAoB,CAACmB,aAAa,EAAED,MAAM,CAAC;IAElE,MAAMD,IAAI,GAAGzB,UAAU,CAAC,CAAC;IAEzB,MAAMqB,SAAS,GAAGkB,KAAK,CAACyC,mBAA6B;IACrD,MAAM1D,MAAM,GAAGP,OAAO,CAACO,MAAM;IAE7B,MAAME,WAAW,GAAGT,OAAO,CAACS,WAAW;IAEvC,MAAMyD,IAAI,GAAG,IAAIf,KAAK,CAACM,QAAQ,EAAE;IAEjC,MAAMU,cAAc,GAAG,GAAGb,OAAO,0BAA0B5C,IAAI,EAAE;IACjE,MAAM0D,cAAc,GAAGpB,IAAI,CAACqB,SAAS;IAErC,MAAMC,aAAa,GAAGC,IAAI,CAACC,KAAK,CAACjE,MAAM,GAAG,GAAG,CAAC;IAC9C,MAAMkE,OAAO,GAAG,MAAMT,cAAc,CAACU,aAAa,CAChDJ,aAAa,EACb7D,WAAW,EACX0D,cAAc,EACd7D,SAAS,EACTM,aAAa,EACb;MAAED,MAAM;MAAEuD;IAAK,CACjB,CAAC;IAED,MAAMS,WAA+B,GAAG;MACtCjE,IAAI;MACJC,MAAM;MACNL,SAAS;MACTC,MAAM;MACNE,WAAW;MACXN,SAAS,EAAEsE,OAAO,CAACtE,SAAS;MAC5B8C,aAAa;MACb2B,SAAS,EAAEpB,UAAU;MACrBqB,UAAU,EAAET,cAAc;MAC1BxD;IACF,CAAC;IAEDkC,OAAO,CAACgC,GAAG,CAACC,GAAG,CAAC,WAAWrE,IAAI,EAAE,EAAEiE,WAAW,CAAC;IAE/C,OAAO5B,CAAC,CAACY,QAAQ,CAACc,OAAO,CAACO,UAAU,CAAC,CAACpB,IAAI,CAAC1E,WAAW,CAAC2E,SAAS,CAAC;EACnE;;EAEA;AACF;AACA;AACA;EACE,MAAMoB,QAAQA,CACZnC,OAA2B,EAC3BoC,SAAuB,EACvBC,OAAoB,EACL;IACf,MAAMjD,YAAY,GAAG,IAAI,CAACX,wBAAwB,CAAC4D,OAAO,CAAC3D,KAAK,CAAC;IAEjE,IAAI,CAACU,YAAY,EAAE;MACjB,MAAM,IAAI3C,mBAAmB,CAC3B,IAAI,EACJ,kCAAkC,EAClC,IAAI,EACJD,iBAAiB,CAAC8F,iBACpB,CAAC;IACH;IAEA,IAAIlD,YAAY,CAACmD,OAAO,EAAEtE,MAAM,KAAK,SAAS,EAAE;MAC9C;IACF;IAEA,MAAM;MAAEZ,SAAS;MAAES,aAAa;MAAED;IAAO,CAAC,GAAGuB,YAAY;IACzD,MAAM8B,cAAc,GAAGvE,oBAAoB,CAACmB,aAAa,EAAED,MAAM,CAAC;;IAElE;AACJ;AACA;IACI,MAAMI,MAAM,GAAG,MAAMiD,cAAc,CAACsB,gBAAgB,CAClDnF,SAAS,EACTS,aACF,CAAC;IAEDpB,sBAAsB,CAAC+F,kBAAkB,CACvCxE,MAAM,CAACR,MAAM,EACb,IAAI,CAACP,OAAO,CAACO,MAAM,EACnB,IACF,CAAC;IAED,IAAIQ,MAAM,CAACS,KAAK,CAACT,MAAM,KAAK,SAAS,EAAE;MACrC,MAAM,IAAI,CAACyE,mBAAmB,CAAC1C,OAAO,EAAEZ,YAAY,CAAC;MACrD;IACF;IAEA,IAAInB,MAAM,CAACS,KAAK,CAACT,MAAM,KAAK,YAAY,EAAE;MACxC,MAAM,IAAIxB,mBAAmB,CAC3B,IAAI,EACJ,gFAAgF,EAChF,IAAI,EACJD,iBAAiB,CAACmG,cACpB,CAAC;IACH;IAEA,MAAMC,QAAQ,GAAG,MAAM1B,cAAc,CAAC2B,cAAc,CAClDxF,SAAS,EACTY,MAAM,CAACR,MACT,CAAC;IAED,IAAI,CAACmF,QAAQ,EAAE;MACb,MAAM,IAAInG,mBAAmB,CAC3B,IAAI,EACJ,qFAAqF,EACrF,KACF,CAAC;IACH;IAEA,MAAM,IAAI,CAACiG,mBAAmB,CAAC1C,OAAO,EAAEZ,YAAY,CAAC;EACvD;;EAEA;AACF;AACA;AACA;EACE,MAAcsD,mBAAmBA,CAC/B1C,OAA2B,EAC3BZ,YAA0B,EACX;IACf,MAAM0D,YAA0B,GAAG;MACjC,GAAG1D,YAAY;MACfmD,OAAO,EAAE;QACPtE,MAAM,EAAE,SAAS;QACjBE,SAAS,EAAE,IAAI4E,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC;MACpC;IACF,CAAC;IAED,IAAI,IAAI,CAACC,IAAI,EAAE;MACb,MAAMC,YAAY,GAAG,MAAM,IAAI,CAACD,IAAI,CAAC1C,QAAQ,CAACP,OAAO,CAAC;MACtD,MAAM,IAAI,CAACiD,IAAI,CAACE,UAAU,CAACnD,OAAO,EAAEkD,YAAY,EAAE;QAChD,CAAC,IAAI,CAACtE,IAAI,GAAGkE;MACf,CAAC,CAAC;IACJ;EACF;AACF;;AAiBA;AACA;AACA","ignoreList":[]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ComponentType } from '@defra/forms-model';
|
|
2
2
|
import Boom from '@hapi/boom';
|
|
3
3
|
import { wait } from '@hapi/hoek';
|
|
4
|
+
import { StatusCodes } from 'http-status-codes';
|
|
4
5
|
import { FileUploadField, tempItemSchema } from "../components/FileUploadField.js";
|
|
5
6
|
import { getCacheService, getError, getExponentialBackoffDelay } from "../helpers.js";
|
|
6
7
|
import { QuestionPageController } from "./QuestionPageController.js";
|
|
@@ -269,7 +270,19 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
269
270
|
return this.initiateAndStoreNewUpload(request, state);
|
|
270
271
|
}
|
|
271
272
|
const uploadId = upload.uploadId;
|
|
272
|
-
|
|
273
|
+
let statusResponse;
|
|
274
|
+
try {
|
|
275
|
+
statusResponse = await getUploadStatus(uploadId);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// if the user loads a file upload page and queries the cached upload, after the upload has
|
|
278
|
+
// expired in CDP, we will get a 404 from the getUploadStatus endpoint.
|
|
279
|
+
// In this case we want to initiate a new upload and return that state, so the form
|
|
280
|
+
// doesn't blow up for the end user.
|
|
281
|
+
if (Boom.isBoom(err) && err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()) {
|
|
282
|
+
return this.initiateAndStoreNewUpload(request, state);
|
|
283
|
+
}
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
273
286
|
if (!statusResponse) {
|
|
274
287
|
throw Boom.badRequest(`Unexpected empty response from getUploadStatus for ${uploadId}`);
|
|
275
288
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","badRequest","uploadStatus","initiated","err","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n const statusResponse = await getUploadStatus(uploadId)\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"FileUploadPageController.js","names":["ComponentType","Boom","wait","StatusCodes","FileUploadField","tempItemSchema","getCacheService","getError","getExponentialBackoffDelay","QuestionPageController","getProxyUrlForLocalDevelopment","getUploadStatus","initiateUpload","FileStatus","UploadStatus","MAX_UPLOADS","CDP_UPLOAD_TIMEOUT_MS","prepareStatus","status","file","form","isPending","fileStatus","pending","errorMessage","prepareFileState","fileState","FileUploadPageController","fileUpload","fileDeleteViewName","constructor","model","pageDef","collection","fileUploads","fields","filter","field","type","at","length","badImplementation","path","indexOf","name","viewName","getStateKeys","component","pagePath","page","getFormDataFromState","request","state","payload","files","getFilesFromState","undefined","getState","refreshUpload","uploadState","upload","getUploadFromState","makeGetItemDeleteRouteHandler","context","h","viewModel","params","fileToRemove","find","uploadId","itemId","notFound","filename","view","backLink","getBackLink","pageTitle","itemTitle","confirmation","text","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","getFormParams","checkRemovedFiles","proceed","getErrors","details","errors","forEach","error","isUploadError","isUploadRootError","push","value","href","getViewModel","components","formComponent","id","index","proxyUrl","uploadUrl","formAction","componentsBefore","slice","checkUploadStatus","depth","initiateAndStoreNewUpload","statusResponse","err","isBoom","output","statusCode","NOT_FOUND","valueOf","badRequest","uploadStatus","initiated","Error","logger","gatewayTimeout","toFixed","delay","info","validationResult","validate","stripUnknown","complete","unshift","mergeState","cacheService","server","setFlash","filesUpdated","options","schema","getFormMetadata","services","formsService","max","Math","min","formMetadata","slug","notificationEmail","newUpload","accept"],"sources":["../../../../../src/server/plugins/engine/pageControllers/FileUploadPageController.ts"],"sourcesContent":["import { ComponentType, type PageFileUpload } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport { wait } from '@hapi/hoek'\nimport { StatusCodes } from 'http-status-codes'\nimport { type ValidationErrorItem } from 'joi'\n\nimport {\n FileUploadField,\n tempItemSchema\n} from '~/src/server/plugins/engine/components/FileUploadField.js'\nimport { type FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport {\n getCacheService,\n getError,\n getExponentialBackoffDelay\n} from '~/src/server/plugins/engine/helpers.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/index.js'\nimport { QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { getProxyUrlForLocalDevelopment } from '~/src/server/plugins/engine/pageControllers/helpers/index.js'\nimport {\n getUploadStatus,\n initiateUpload\n} from '~/src/server/plugins/engine/services/uploadService.js'\nimport {\n FileStatus,\n UploadStatus,\n type AnyFormRequest,\n type FeaturedFormPageViewModel,\n type FileState,\n type FormContext,\n type FormContextRequest,\n type FormSubmissionError,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type UploadInitiateResponse,\n type UploadStatusFileResponse\n} from '~/src/server/plugins/engine/types.js'\nimport {\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nconst MAX_UPLOADS = 25\nconst CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute\n\nexport function prepareStatus(status: UploadStatusFileResponse) {\n const file = status.form.file\n const isPending = file.fileStatus === FileStatus.pending\n\n if (!file.errorMessage && isPending) {\n file.errorMessage = 'The selected file has not fully uploaded'\n }\n\n return status\n}\n\nfunction prepareFileState(fileState: FileState) {\n prepareStatus(fileState.status)\n\n return fileState\n}\n\nexport class FileUploadPageController extends QuestionPageController {\n declare pageDef: PageFileUpload\n\n fileUpload: FileUploadField\n fileDeleteViewName = 'item-delete'\n\n constructor(model: FormModel, pageDef: PageFileUpload) {\n super(model, pageDef)\n\n const { collection } = this\n\n // Get the file upload fields from the collection\n const fileUploads = collection.fields.filter(\n (field): field is FileUploadField =>\n field.type === ComponentType.FileUploadField\n )\n\n const fileUpload = fileUploads.at(0)\n\n // Assert we have exactly 1 file upload component\n if (!fileUpload || fileUploads.length > 1) {\n throw Boom.badImplementation(\n `Expected 1 FileUploadFieldComponent in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assert the file upload component is the first form component\n if (collection.fields.indexOf(fileUpload) !== 0) {\n throw Boom.badImplementation(\n `Expected '${fileUpload.name}' to be the first form component in FileUploadPageController '${pageDef.path}'`\n )\n }\n\n // Assign the file upload component to the controller\n this.fileUpload = fileUpload\n this.viewName = 'file-upload'\n }\n\n /**\n * Get supplementary state keys for clearing file upload state.\n * Returns the nested upload path for FileUploadField components only.\n * @param component - The component to get supplementary state keys for\n * @returns Array containing the nested upload path, e.g., [\"upload['/page-path']\"]\n * or ['upload'] if no page path is available. Returns empty array for non-FileUploadField components.\n */\n getStateKeys(component: FormComponent): string[] {\n // Only return upload keys for FileUploadField components\n if (!(component instanceof FileUploadField)) {\n return []\n }\n\n const pagePath = component.page?.path\n return pagePath ? [`upload['${pagePath}']`] : ['upload']\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { fileUpload } = this\n\n const payload = super.getFormDataFromState(request, state)\n const files = this.getFilesFromState(state)\n\n // Append the files to the payload\n payload[fileUpload.name] = files.length ? files : undefined\n\n return payload\n }\n\n async getState(request: AnyFormRequest) {\n const { fileUpload } = this\n\n // Get the actual state\n const state = await super.getState(request)\n const files = this.getFilesFromState(state)\n\n // Overwrite the files with those in the upload state\n state[fileUpload.name] = files\n\n return this.refreshUpload(request, state)\n }\n\n /**\n * Get the uploaded files from state.\n */\n getFilesFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.files ?? []\n }\n\n /**\n * Get the initiated upload from state.\n */\n getUploadFromState(state: FormSubmissionState) {\n const { path } = this\n\n const uploadState = state.upload?.[path]\n return uploadState?.upload\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { params } = request\n const { state } = context\n\n const files = this.getFilesFromState(state)\n\n const fileToRemove = files.find(\n ({ uploadId }) => uploadId === params.itemId\n )\n\n if (!fileToRemove) {\n throw Boom.notFound('File to delete not found')\n }\n\n const { filename } = fileToRemove.status.form.file\n\n return h.view(this.fileDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this file?`,\n itemTitle: filename,\n confirmation: { text: 'You cannot recover removed files.' },\n buttonConfirm: { text: 'Remove file' },\n buttonCancel: { text: 'Cancel' }\n } satisfies ItemDeletePageViewModel)\n }\n }\n\n makePostItemDeleteRouteHandler() {\n return async (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n // Check for any removed files in the POST payload\n if (confirm) {\n await this.checkRemovedFiles(request, state)\n return this.proceed(request, h, path)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getErrors(details?: ValidationErrorItem[]) {\n const { fileUpload } = this\n\n if (details) {\n const errors: FormSubmissionError[] = []\n\n details.forEach((error) => {\n const isUploadError = error.path[0] === fileUpload.name\n const isUploadRootError = isUploadError && error.path.length === 1\n\n if (!isUploadError || isUploadRootError) {\n // The error is for the root of the upload or another\n // field on the page so defer to the getError helper\n errors.push(getError(error))\n } else {\n const { context, path, type } = error\n\n if (type === 'object.unknown' && path.at(-1) === 'errorMessage') {\n const value = context?.value as string | undefined\n\n if (value) {\n const name = fileUpload.name\n const text = typeof value === 'string' ? value : 'Unknown error'\n const href = `#${name}`\n\n errors.push({ path, href, name, text })\n }\n }\n }\n })\n\n return errors\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FeaturedFormPageViewModel {\n const { fileUpload } = this\n const { state } = context\n\n const upload = this.getUploadFromState(state)\n\n const viewModel = super.getViewModel(request, context)\n const { components } = viewModel\n\n // Featured form component\n const [formComponent] = components.filter(\n ({ model }) => model.id === fileUpload.name\n )\n\n const index = components.indexOf(formComponent)\n\n const proxyUrl = getProxyUrlForLocalDevelopment(upload?.uploadUrl)\n\n return {\n ...viewModel,\n formAction: upload?.uploadUrl,\n uploadId: upload?.uploadId,\n formComponent,\n\n // Split out components before/after\n componentsBefore: components.slice(0, index),\n components: components.slice(index),\n proxyUrl\n }\n }\n\n /**\n * Refreshes the CDP upload and files in the\n * state and checks for any removed files.\n *\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise we initiate a new one.\n * @param request - the hapi request\n * @param state - the form state\n */\n private async refreshUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n state = await this.checkUploadStatus(request, state)\n\n return state\n }\n\n /**\n * If an upload exists and hasn't been consumed\n * it gets re-used, otherwise a new one is initiated.\n * @param request - the hapi request\n * @param state - the form state\n * @param depth - the number of retries so far\n */\n private async checkUploadStatus(\n request: AnyFormRequest,\n state: FormSubmissionState,\n depth = 1\n ): Promise<FormSubmissionState> {\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n // If no upload exists, initiate a new one.\n if (!upload?.uploadId) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const uploadId = upload.uploadId\n\n let statusResponse\n\n try {\n statusResponse = await getUploadStatus(uploadId)\n } catch (err) {\n // if the user loads a file upload page and queries the cached upload, after the upload has\n // expired in CDP, we will get a 404 from the getUploadStatus endpoint.\n // In this case we want to initiate a new upload and return that state, so the form\n // doesn't blow up for the end user.\n if (\n Boom.isBoom(err) &&\n err.output.statusCode === StatusCodes.NOT_FOUND.valueOf()\n ) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n throw err\n }\n\n if (!statusResponse) {\n throw Boom.badRequest(\n `Unexpected empty response from getUploadStatus for ${uploadId}`\n )\n }\n\n // Re-use the upload if it is still in the \"initiated\" state.\n if (statusResponse.uploadStatus === UploadStatus.initiated) {\n return state\n }\n\n if (statusResponse.uploadStatus === UploadStatus.pending) {\n // Using exponential backoff delays:\n // Depth 1: 2000ms, Depth 2: 4000ms, Depth 3: 8000ms, Depth 4: 16000ms, Depth 5+: 30000ms (capped)\n // A depth of 5 (or more) implies cumulative delays roughly reaching 55 seconds.\n if (depth >= 5) {\n const err = new Error(\n `Exceeded cumulative retry delay for ${uploadId} (depth: ${depth}). Re-initiating a new upload.`\n )\n request.logger.error(\n err,\n `[uploadTimeout] Exceeded cumulative retry delay for uploadId: ${uploadId} at depth: ${depth} - re-initiating new upload`\n )\n await this.initiateAndStoreNewUpload(request, state)\n throw Boom.gatewayTimeout(\n `Timed out waiting for ${uploadId} after cumulative retries exceeding ${((CDP_UPLOAD_TIMEOUT_MS - 5000) / 1000).toFixed(0)} seconds`\n )\n }\n const delay = getExponentialBackoffDelay(depth)\n request.logger.info(\n `[uploadRetry] Waiting ${delay / 1000} seconds for uploadId: ${uploadId} to complete (retry depth: ${depth})`\n )\n await wait(delay)\n return this.checkUploadStatus(request, state, depth + 1)\n }\n\n // Only add to files state if the file validates.\n // This secures against html tampering of the file input\n // by adding a 'multiple' attribute or it being\n // changed to a simple text field or similar.\n const validationResult = tempItemSchema.validate(\n { uploadId, status: statusResponse },\n { stripUnknown: true }\n )\n const error = validationResult.error\n const fileState = validationResult.value as FileState\n\n if (error) {\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n const file = fileState.status.form.file\n if (file.fileStatus === FileStatus.complete) {\n files.unshift(prepareFileState(fileState))\n await this.mergeState(request, state, {\n upload: { [this.path]: { files, upload } }\n })\n } else {\n // Flash the error message.\n const { fileUpload } = this\n const cacheService = getCacheService(request.server)\n\n const name = fileUpload.name\n const text = file.errorMessage ?? 'Unknown error'\n const errors: FormSubmissionError[] = [\n { path: [name], href: `#${name}`, name, text }\n ]\n cacheService.setFlash(request, { errors })\n }\n\n return this.initiateAndStoreNewUpload(request, state)\n }\n\n /**\n * Checks the payload for a file getting removed\n * and removes it from the upload files if found\n * @param request - the hapi request\n * @param state - the form state\n * @returns updated state if any files have been removed\n */\n private async checkRemovedFiles(\n request: FormRequestPayload,\n state: FormSubmissionState\n ) {\n const { path } = this\n const { params } = request\n\n const upload = this.getUploadFromState(state)\n const files = this.getFilesFromState(state)\n\n const filesUpdated = files.filter(\n ({ uploadId }) => uploadId !== params.itemId\n )\n\n if (filesUpdated.length === files.length) {\n return\n }\n\n await this.mergeState(request, state, {\n upload: { [path]: { files: filesUpdated, upload } }\n })\n }\n\n /**\n * Initiates a CDP file upload and stores in the upload state\n * @param request - the hapi request\n * @param state - the form state\n */\n private async initiateAndStoreNewUpload(\n request: AnyFormRequest,\n state: FormSubmissionState\n ) {\n const { fileUpload, href, path } = this\n const { options, schema } = fileUpload\n const { getFormMetadata } = this.model.services.formsService\n\n const files = this.getFilesFromState(state)\n\n // Reset the upload in state\n let upload: UploadInitiateResponse | undefined\n\n // Don't initiate anymore after minimum of `schema.max` or MAX_UPLOADS\n const max = Math.min(schema.max ?? MAX_UPLOADS, MAX_UPLOADS)\n\n if (files.length < max) {\n const formMetadata = await getFormMetadata(request.params.slug)\n const notificationEmail =\n formMetadata.notificationEmail ?? 'defraforms@defra.gov.uk'\n\n const newUpload = await initiateUpload(\n href,\n notificationEmail,\n options.accept\n )\n\n if (newUpload === undefined) {\n throw Boom.badRequest('Unexpected empty response from initiateUpload')\n }\n\n upload = newUpload\n }\n\n return this.mergeState(request, state, {\n upload: { [path]: { files, upload } }\n })\n }\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
|