@defra/forms-engine-plugin 4.0.47 → 4.0.49
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 +1 -1
- package/.server/server/plugins/engine/components/PaymentField.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js +3 -3
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +5 -5
- package/.server/server/plugins/engine/pageControllers/RepeatPageController.js +6 -6
- package/.server/server/plugins/engine/pageControllers/RepeatPageController.js.map +1 -1
- package/.server/server/plugins/engine/views/repeat-list-summary.html +1 -1
- package/.server/server/plugins/payment/service.d.ts +2 -1
- package/.server/server/plugins/payment/service.js +27 -4
- package/.server/server/plugins/payment/service.js.map +1 -1
- package/.server/server/plugins/payment/service.test.js +4 -4
- package/.server/server/plugins/payment/service.test.js.map +1 -1
- package/package.json +1 -1
- package/src/server/plugins/engine/components/PaymentField.ts +4 -1
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +11 -11
- package/src/server/plugins/engine/models/SummaryViewModel.ts +3 -3
- package/src/server/plugins/engine/models/__snapshots__/SummaryViewModel.test.ts.snap +5 -5
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +3 -3
- package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +7 -5
- package/src/server/plugins/engine/views/repeat-list-summary.html +1 -1
- package/src/server/plugins/payment/service.js +28 -2
- package/src/server/plugins/payment/service.test.js +13 -4
|
@@ -194,7 +194,7 @@ export class PaymentField extends FormComponent {
|
|
|
194
194
|
if (status.state.status !== 'capturable') {
|
|
195
195
|
throw new PaymentPreAuthError(this, 'Your payment authorisation has expired. Please add your payment details again.', true, PaymentErrorTypes.PaymentExpired);
|
|
196
196
|
}
|
|
197
|
-
const captured = await paymentService.capturePayment(paymentId);
|
|
197
|
+
const captured = await paymentService.capturePayment(paymentId, status.amount);
|
|
198
198
|
if (!captured) {
|
|
199
199
|
throw new PaymentPreAuthError(this, 'There was a problem and your form was not submitted. Try submitting the form again.', false);
|
|
200
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PaymentField.js","names":["randomUUID","StatusCodes","joi","FormComponent","getPluginOptions","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","createPaymentService","PaymentField","isAppendageStateSingleObject","constructor","def","props","options","paymentStateSchema","object","paymentId","string","required","reference","amount","number","description","uuid","formId","isLivePayment","boolean","preAuth","status","valid","createdAt","isoDate","unknown","label","formSchema","stateSchema","getPaymentStateFromState","state","value","name","isPaymentState","undefined","getDisplayStringFromState","toFixed","getViewModel","payload","errors","viewModel","paymentState","formattedAmount","Array","isArray","isState","getFormValue","getContextValueFromState","getAllPossibleErrors","baseErrors","type","template","advancedSettingsErrors","dispatcher","request","h","args","componentName","component","model","controller","getState","baseUrl","server","summaryUrl","basePath","existingPaymentState","redirect","code","SEE_OTHER","isLive","isPreview","paymentService","$$__referenceNumber","slug","payCallbackUrl","paymentPageUrl","sourceUrl","amountInPence","Math","round","payment","createPayment","sessionData","returnUrl","failureUrl","yar","set","paymentUrl","onSubmit","_metadata","context","PaymentIncomplete","capture","getPaymentStatus","checkPaymentAmount","markPaymentCaptured","PaymentExpired","captured","capturePayment","updatedState","Date","toISOString","page","currentState","mergeState"],"sources":["../../../../../src/server/plugins/engine/components/PaymentField.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nimport {\n type FormMetadata,\n type PaymentFieldComponent\n} from '@defra/forms-model'\nimport { StatusCodes } from 'http-status-codes'\nimport joi, { type ObjectSchema } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'\nimport {\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/plugins/engine/types/index.js'\nimport {\n type ErrorMessageTemplateList,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { createPaymentService } from '~/src/server/plugins/payment/helper.js'\n\nexport class PaymentField extends FormComponent {\n declare options: PaymentFieldComponent['options']\n declare formSchema: ObjectSchema\n declare stateSchema: ObjectSchema\n isAppendageStateSingleObject = true\n\n constructor(\n def: PaymentFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n this.options = def.options\n\n const paymentStateSchema = joi\n .object({\n paymentId: joi.string().required(),\n reference: joi.string().required(),\n amount: joi.number().required(),\n description: joi.string().required(),\n uuid: joi.string().uuid().required(),\n formId: joi.string().required(),\n isLivePayment: joi.boolean().required(),\n preAuth: joi\n .object({\n status: joi\n .string()\n .valid('success', 'failed', 'started')\n .required(),\n createdAt: joi.string().isoDate().required()\n })\n .required()\n })\n .unknown(true)\n .label(this.label)\n\n this.formSchema = paymentStateSchema\n // 'required()' forces the payment page to be invalid until we have valid payment state\n // i.e. the user will automatically be directed back to the payment page\n // if they attempt to access future pages when no payment entered yet\n this.stateSchema = paymentStateSchema.required()\n }\n\n /**\n * Gets the PaymentState from form submission state\n */\n getPaymentStateFromState(\n state: FormSubmissionState\n ): PaymentState | undefined {\n const value = state[this.name]\n return this.isPaymentState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState): string {\n const value = this.getPaymentStateFromState(state)\n\n if (!value) {\n return ''\n }\n\n return `£${value.amount.toFixed(2)} - ${value.description}`\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n\n // Payload is pre-populated from state if a payment has already been made\n const paymentState = this.isPaymentState(payload[this.name] as unknown)\n ? (payload[this.name] as unknown as PaymentState)\n : undefined\n\n // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.\n const amount = paymentState?.amount ?? this.options.amount\n\n const formattedAmount = amount.toFixed(2)\n\n return {\n ...viewModel,\n amount: formattedAmount,\n description: this.options.description,\n paymentState\n }\n }\n\n /**\n * Type guard to check if value is PaymentState\n */\n isPaymentState(value: unknown): value is PaymentState {\n return PaymentField.isPaymentState(value)\n }\n\n /**\n * Static type guard to check if value is PaymentState\n */\n static isPaymentState(value: unknown): value is PaymentState {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false\n }\n\n const state = value as PaymentState\n return (\n typeof state.paymentId === 'string' &&\n typeof state.amount === 'number' &&\n typeof state.description === 'string'\n )\n }\n\n /**\n * Override base isState to validate PaymentState\n */\n isState(value?: FormStateValue | FormState): value is FormState {\n return this.isPaymentState(value)\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isPaymentState(value)\n ? (value as unknown as NonNullable<FormStateValue>)\n : undefined\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n return this.isPaymentState(state)\n ? `Reference: ${state.reference}\\nAmount: ${state.amount.toFixed(2)}`\n : ''\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return PaymentField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n {\n type: 'paymentRequired',\n template: 'Complete the payment to continue'\n }\n ],\n advancedSettingsErrors: []\n }\n }\n\n /**\n * Dispatcher for external redirect to GOV.UK Pay\n */\n static async dispatcher(\n request: FormRequestPayload,\n h: FormResponseToolkit,\n args: PaymentDispatcherArgs\n ): Promise<unknown> {\n const { options, name: componentName } = args.component\n const { model } = args.controller\n\n const state = await args.controller.getState(request)\n const { baseUrl } = getPluginOptions(request.server)\n const summaryUrl = `${baseUrl}/${model.basePath}/summary`\n\n const existingPaymentState = state[componentName]\n if (\n PaymentField.isPaymentState(existingPaymentState) &&\n existingPaymentState.preAuth?.status === 'success'\n ) {\n return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)\n }\n\n const isLivePayment = args.isLive && !args.isPreview\n const formId = args.controller.model.formId\n const paymentService = createPaymentService(isLivePayment, formId)\n\n const uuid = randomUUID()\n\n const reference = state.$$__referenceNumber as string\n const amount = options.amount\n\n const description = options.description\n\n const slug = `/${model.basePath}`\n\n const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`\n const paymentPageUrl = args.sourceUrl\n\n const amountInPence = Math.round(amount * 100)\n const payment = await paymentService.createPayment(\n amountInPence,\n description,\n payCallbackUrl,\n reference,\n { formId, slug }\n )\n\n const sessionData: PaymentSessionData = {\n uuid,\n formId,\n reference,\n amount,\n description,\n paymentId: payment.paymentId,\n componentName,\n returnUrl: summaryUrl,\n failureUrl: paymentPageUrl,\n isLivePayment\n }\n\n request.yar.set(`payment-${uuid}`, sessionData)\n\n return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)\n }\n\n /**\n * Called on form submission to capture the payment\n * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment\n */\n async onSubmit(\n request: FormRequestPayload,\n _metadata: FormMetadata,\n context: FormContext\n ): Promise<void> {\n const paymentState = this.getPaymentStateFromState(context.state)\n\n if (!paymentState) {\n throw new PaymentPreAuthError(\n this,\n 'Complete the payment to continue',\n true,\n PaymentErrorTypes.PaymentIncomplete\n )\n }\n\n if (paymentState.capture?.status === 'success') {\n return\n }\n\n const { paymentId, isLivePayment, formId } = paymentState\n const paymentService = createPaymentService(isLivePayment, formId)\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const status = await paymentService.getPaymentStatus(paymentId)\n\n PaymentSubmissionError.checkPaymentAmount(\n status.amount,\n this.options.amount,\n this\n )\n\n if (status.state.status === 'success') {\n await this.markPaymentCaptured(request, paymentState)\n return\n }\n\n if (status.state.status !== 'capturable') {\n throw new PaymentPreAuthError(\n this,\n 'Your payment authorisation has expired. Please add your payment details again.',\n true,\n PaymentErrorTypes.PaymentExpired\n )\n }\n\n const captured = await paymentService.capturePayment(paymentId)\n\n if (!captured) {\n throw new PaymentPreAuthError(\n this,\n 'There was a problem and your form was not submitted. Try submitting the form again.',\n false\n )\n }\n\n await this.markPaymentCaptured(request, paymentState)\n }\n\n /**\n * Updates payment state to mark capture as successful\n * This ensures we don't try to re-capture on submission retry\n */\n private async markPaymentCaptured(\n request: FormRequestPayload,\n paymentState: PaymentState\n ): Promise<void> {\n const updatedState: PaymentState = {\n ...paymentState,\n capture: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n if (this.page) {\n const currentState = await this.page.getState(request)\n await this.page.mergeState(request, currentState, {\n [this.name]: updatedState\n })\n }\n }\n}\n\nexport interface PaymentDispatcherArgs {\n controller: {\n model: {\n formId: string\n basePath: string\n name: string\n }\n getState: (request: AnyFormRequest) => Promise<FormSubmissionState>\n }\n component: PaymentField\n sourceUrl: string\n isLive: boolean\n isPreview: boolean\n}\n\n/**\n * Session data stored when dispatching to GOV.UK Pay\n */\nexport interface PaymentSessionData {\n uuid: string\n formId: string\n reference: string\n amount: number\n description: string\n paymentId: string\n componentName: string\n returnUrl: string\n failureUrl: string\n isLivePayment: boolean\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"PaymentField.js","names":["randomUUID","StatusCodes","joi","FormComponent","getPluginOptions","PaymentErrorTypes","PaymentPreAuthError","PaymentSubmissionError","createPaymentService","PaymentField","isAppendageStateSingleObject","constructor","def","props","options","paymentStateSchema","object","paymentId","string","required","reference","amount","number","description","uuid","formId","isLivePayment","boolean","preAuth","status","valid","createdAt","isoDate","unknown","label","formSchema","stateSchema","getPaymentStateFromState","state","value","name","isPaymentState","undefined","getDisplayStringFromState","toFixed","getViewModel","payload","errors","viewModel","paymentState","formattedAmount","Array","isArray","isState","getFormValue","getContextValueFromState","getAllPossibleErrors","baseErrors","type","template","advancedSettingsErrors","dispatcher","request","h","args","componentName","component","model","controller","getState","baseUrl","server","summaryUrl","basePath","existingPaymentState","redirect","code","SEE_OTHER","isLive","isPreview","paymentService","$$__referenceNumber","slug","payCallbackUrl","paymentPageUrl","sourceUrl","amountInPence","Math","round","payment","createPayment","sessionData","returnUrl","failureUrl","yar","set","paymentUrl","onSubmit","_metadata","context","PaymentIncomplete","capture","getPaymentStatus","checkPaymentAmount","markPaymentCaptured","PaymentExpired","captured","capturePayment","updatedState","Date","toISOString","page","currentState","mergeState"],"sources":["../../../../../src/server/plugins/engine/components/PaymentField.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nimport {\n type FormMetadata,\n type PaymentFieldComponent\n} from '@defra/forms-model'\nimport { StatusCodes } from 'http-status-codes'\nimport joi, { type ObjectSchema } from 'joi'\n\nimport { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport { getPluginOptions } from '~/src/server/plugins/engine/helpers.js'\nimport {\n PaymentErrorTypes,\n PaymentPreAuthError,\n PaymentSubmissionError\n} from '~/src/server/plugins/engine/pageControllers/errors.js'\nimport {\n type AnyFormRequest,\n type FormContext,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/plugins/engine/types/index.js'\nimport {\n type ErrorMessageTemplateList,\n type FormPayload,\n type FormState,\n type FormStateValue,\n type FormSubmissionError,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\nimport { createPaymentService } from '~/src/server/plugins/payment/helper.js'\n\nexport class PaymentField extends FormComponent {\n declare options: PaymentFieldComponent['options']\n declare formSchema: ObjectSchema\n declare stateSchema: ObjectSchema\n isAppendageStateSingleObject = true\n\n constructor(\n def: PaymentFieldComponent,\n props: ConstructorParameters<typeof FormComponent>[1]\n ) {\n super(def, props)\n\n this.options = def.options\n\n const paymentStateSchema = joi\n .object({\n paymentId: joi.string().required(),\n reference: joi.string().required(),\n amount: joi.number().required(),\n description: joi.string().required(),\n uuid: joi.string().uuid().required(),\n formId: joi.string().required(),\n isLivePayment: joi.boolean().required(),\n preAuth: joi\n .object({\n status: joi\n .string()\n .valid('success', 'failed', 'started')\n .required(),\n createdAt: joi.string().isoDate().required()\n })\n .required()\n })\n .unknown(true)\n .label(this.label)\n\n this.formSchema = paymentStateSchema\n // 'required()' forces the payment page to be invalid until we have valid payment state\n // i.e. the user will automatically be directed back to the payment page\n // if they attempt to access future pages when no payment entered yet\n this.stateSchema = paymentStateSchema.required()\n }\n\n /**\n * Gets the PaymentState from form submission state\n */\n getPaymentStateFromState(\n state: FormSubmissionState\n ): PaymentState | undefined {\n const value = state[this.name]\n return this.isPaymentState(value) ? value : undefined\n }\n\n getDisplayStringFromState(state: FormSubmissionState): string {\n const value = this.getPaymentStateFromState(state)\n\n if (!value) {\n return ''\n }\n\n return `£${value.amount.toFixed(2)} - ${value.description}`\n }\n\n getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {\n const viewModel = super.getViewModel(payload, errors)\n\n // Payload is pre-populated from state if a payment has already been made\n const paymentState = this.isPaymentState(payload[this.name] as unknown)\n ? (payload[this.name] as unknown as PaymentState)\n : undefined\n\n // When user initially visits the payment page, there is no payment state yet so the amount is read form the form definition.\n const amount = paymentState?.amount ?? this.options.amount\n\n const formattedAmount = amount.toFixed(2)\n\n return {\n ...viewModel,\n amount: formattedAmount,\n description: this.options.description,\n paymentState\n }\n }\n\n /**\n * Type guard to check if value is PaymentState\n */\n isPaymentState(value: unknown): value is PaymentState {\n return PaymentField.isPaymentState(value)\n }\n\n /**\n * Static type guard to check if value is PaymentState\n */\n static isPaymentState(value: unknown): value is PaymentState {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return false\n }\n\n const state = value as PaymentState\n return (\n typeof state.paymentId === 'string' &&\n typeof state.amount === 'number' &&\n typeof state.description === 'string'\n )\n }\n\n /**\n * Override base isState to validate PaymentState\n */\n isState(value?: FormStateValue | FormState): value is FormState {\n return this.isPaymentState(value)\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isPaymentState(value)\n ? (value as unknown as NonNullable<FormStateValue>)\n : undefined\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n return this.isPaymentState(state)\n ? `Reference: ${state.reference}\\nAmount: ${state.amount.toFixed(2)}`\n : ''\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return PaymentField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n return {\n baseErrors: [\n {\n type: 'paymentRequired',\n template: 'Complete the payment to continue'\n }\n ],\n advancedSettingsErrors: []\n }\n }\n\n /**\n * Dispatcher for external redirect to GOV.UK Pay\n */\n static async dispatcher(\n request: FormRequestPayload,\n h: FormResponseToolkit,\n args: PaymentDispatcherArgs\n ): Promise<unknown> {\n const { options, name: componentName } = args.component\n const { model } = args.controller\n\n const state = await args.controller.getState(request)\n const { baseUrl } = getPluginOptions(request.server)\n const summaryUrl = `${baseUrl}/${model.basePath}/summary`\n\n const existingPaymentState = state[componentName]\n if (\n PaymentField.isPaymentState(existingPaymentState) &&\n existingPaymentState.preAuth?.status === 'success'\n ) {\n return h.redirect(summaryUrl).code(StatusCodes.SEE_OTHER)\n }\n\n const isLivePayment = args.isLive && !args.isPreview\n const formId = args.controller.model.formId\n const paymentService = createPaymentService(isLivePayment, formId)\n\n const uuid = randomUUID()\n\n const reference = state.$$__referenceNumber as string\n const amount = options.amount\n\n const description = options.description\n\n const slug = `/${model.basePath}`\n\n const payCallbackUrl = `${baseUrl}/payment-callback?uuid=${uuid}`\n const paymentPageUrl = args.sourceUrl\n\n const amountInPence = Math.round(amount * 100)\n const payment = await paymentService.createPayment(\n amountInPence,\n description,\n payCallbackUrl,\n reference,\n { formId, slug }\n )\n\n const sessionData: PaymentSessionData = {\n uuid,\n formId,\n reference,\n amount,\n description,\n paymentId: payment.paymentId,\n componentName,\n returnUrl: summaryUrl,\n failureUrl: paymentPageUrl,\n isLivePayment\n }\n\n request.yar.set(`payment-${uuid}`, sessionData)\n\n return h.redirect(payment.paymentUrl).code(StatusCodes.SEE_OTHER)\n }\n\n /**\n * Called on form submission to capture the payment\n * @see https://docs.payments.service.gov.uk/delayed_capture/#delay-taking-a-payment\n */\n async onSubmit(\n request: FormRequestPayload,\n _metadata: FormMetadata,\n context: FormContext\n ): Promise<void> {\n const paymentState = this.getPaymentStateFromState(context.state)\n\n if (!paymentState) {\n throw new PaymentPreAuthError(\n this,\n 'Complete the payment to continue',\n true,\n PaymentErrorTypes.PaymentIncomplete\n )\n }\n\n if (paymentState.capture?.status === 'success') {\n return\n }\n\n const { paymentId, isLivePayment, formId } = paymentState\n const paymentService = createPaymentService(isLivePayment, formId)\n\n /**\n * @see https://docs.payments.service.gov.uk/api_reference/#payment-status-lifecycle\n */\n const status = await paymentService.getPaymentStatus(paymentId)\n\n PaymentSubmissionError.checkPaymentAmount(\n status.amount,\n this.options.amount,\n this\n )\n\n if (status.state.status === 'success') {\n await this.markPaymentCaptured(request, paymentState)\n return\n }\n\n if (status.state.status !== 'capturable') {\n throw new PaymentPreAuthError(\n this,\n 'Your payment authorisation has expired. Please add your payment details again.',\n true,\n PaymentErrorTypes.PaymentExpired\n )\n }\n\n const captured = await paymentService.capturePayment(\n paymentId,\n status.amount\n )\n\n if (!captured) {\n throw new PaymentPreAuthError(\n this,\n 'There was a problem and your form was not submitted. Try submitting the form again.',\n false\n )\n }\n\n await this.markPaymentCaptured(request, paymentState)\n }\n\n /**\n * Updates payment state to mark capture as successful\n * This ensures we don't try to re-capture on submission retry\n */\n private async markPaymentCaptured(\n request: FormRequestPayload,\n paymentState: PaymentState\n ): Promise<void> {\n const updatedState: PaymentState = {\n ...paymentState,\n capture: {\n status: 'success',\n createdAt: new Date().toISOString()\n }\n }\n\n if (this.page) {\n const currentState = await this.page.getState(request)\n await this.page.mergeState(request, currentState, {\n [this.name]: updatedState\n })\n }\n }\n}\n\nexport interface PaymentDispatcherArgs {\n controller: {\n model: {\n formId: string\n basePath: string\n name: string\n }\n getState: (request: AnyFormRequest) => Promise<FormSubmissionState>\n }\n component: PaymentField\n sourceUrl: string\n isLive: boolean\n isPreview: boolean\n}\n\n/**\n * Session data stored when dispatching to GOV.UK Pay\n */\nexport interface PaymentSessionData {\n uuid: string\n formId: string\n reference: string\n amount: number\n description: string\n paymentId: string\n componentName: string\n returnUrl: string\n failureUrl: string\n isLivePayment: boolean\n}\n"],"mappings":"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,SAASC,oBAAoB;AAE7B,OAAO,MAAMC,YAAY,SAASN,aAAa,CAAC;EAI9CO,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,GAAGb,GAAG,CAC3Bc,MAAM,CAAC;MACNC,SAAS,EAAEf,GAAG,CAACgB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCC,SAAS,EAAElB,GAAG,CAACgB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAClCE,MAAM,EAAEnB,GAAG,CAACoB,MAAM,CAAC,CAAC,CAACH,QAAQ,CAAC,CAAC;MAC/BI,WAAW,EAAErB,GAAG,CAACgB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MACpCK,IAAI,EAAEtB,GAAG,CAACgB,MAAM,CAAC,CAAC,CAACM,IAAI,CAAC,CAAC,CAACL,QAAQ,CAAC,CAAC;MACpCM,MAAM,EAAEvB,GAAG,CAACgB,MAAM,CAAC,CAAC,CAACC,QAAQ,CAAC,CAAC;MAC/BO,aAAa,EAAExB,GAAG,CAACyB,OAAO,CAAC,CAAC,CAACR,QAAQ,CAAC,CAAC;MACvCS,OAAO,EAAE1B,GAAG,CACTc,MAAM,CAAC;QACNa,MAAM,EAAE3B,GAAG,CACRgB,MAAM,CAAC,CAAC,CACRY,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC,CACrCX,QAAQ,CAAC,CAAC;QACbY,SAAS,EAAE7B,GAAG,CAACgB,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,IAAIA,KAAK,CAAClB,MAAM,CAACuB,OAAO,CAAC,CAAC,CAAC,MAAML,KAAK,CAAChB,WAAW,EAAE;EAC7D;EAEAsB,YAAYA,CAACC,OAAoB,EAAEC,MAA8B,EAAE;IACjE,MAAMC,SAAS,GAAG,KAAK,CAACH,YAAY,CAACC,OAAO,EAAEC,MAAM,CAAC;;IAErD;IACA,MAAME,YAAY,GAAG,IAAI,CAACR,cAAc,CAACK,OAAO,CAAC,IAAI,CAACN,IAAI,CAAY,CAAC,GAClEM,OAAO,CAAC,IAAI,CAACN,IAAI,CAAC,GACnBE,SAAS;;IAEb;IACA,MAAMrB,MAAM,GAAG4B,YAAY,EAAE5B,MAAM,IAAI,IAAI,CAACP,OAAO,CAACO,MAAM;IAE1D,MAAM6B,eAAe,GAAG7B,MAAM,CAACuB,OAAO,CAAC,CAAC,CAAC;IAEzC,OAAO;MACL,GAAGI,SAAS;MACZ3B,MAAM,EAAE6B,eAAe;MACvB3B,WAAW,EAAE,IAAI,CAACT,OAAO,CAACS,WAAW;MACrC0B;IACF,CAAC;EACH;;EAEA;AACF;AACA;EACER,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,IAAIY,KAAK,CAACC,OAAO,CAACb,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;EACE8B,OAAOA,CAACd,KAAkC,EAAsB;IAC9D,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC;EACnC;EAEAe,YAAYA,CAACf,KAAkC,EAAE;IAC/C,OAAO,IAAI,CAACE,cAAc,CAACF,KAAK,CAAC,GAC5BA,KAAK,GACNG,SAAS;EACf;EAEAa,wBAAwBA,CAACjB,KAA0B,EAAE;IACnD,OAAO,IAAI,CAACG,cAAc,CAACH,KAAK,CAAC,GAC7B,cAAcA,KAAK,CAAClB,SAAS,aAAakB,KAAK,CAACjB,MAAM,CAACuB,OAAO,CAAC,CAAC,CAAC,EAAE,GACnE,EAAE;EACR;;EAEA;AACF;AACA;EACEY,oBAAoBA,CAAA,EAA6B;IAC/C,OAAO/C,YAAY,CAAC+C,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;MAAElD,OAAO;MAAE0B,IAAI,EAAEyB;IAAc,CAAC,GAAGD,IAAI,CAACE,SAAS;IACvD,MAAM;MAAEC;IAAM,CAAC,GAAGH,IAAI,CAACI,UAAU;IAEjC,MAAM9B,KAAK,GAAG,MAAM0B,IAAI,CAACI,UAAU,CAACC,QAAQ,CAACP,OAAO,CAAC;IACrD,MAAM;MAAEQ;IAAQ,CAAC,GAAGlE,gBAAgB,CAAC0D,OAAO,CAACS,MAAM,CAAC;IACpD,MAAMC,UAAU,GAAG,GAAGF,OAAO,IAAIH,KAAK,CAACM,QAAQ,UAAU;IAEzD,MAAMC,oBAAoB,GAAGpC,KAAK,CAAC2B,aAAa,CAAC;IACjD,IACExD,YAAY,CAACgC,cAAc,CAACiC,oBAAoB,CAAC,IACjDA,oBAAoB,CAAC9C,OAAO,EAAEC,MAAM,KAAK,SAAS,EAClD;MACA,OAAOkC,CAAC,CAACY,QAAQ,CAACH,UAAU,CAAC,CAACI,IAAI,CAAC3E,WAAW,CAAC4E,SAAS,CAAC;IAC3D;IAEA,MAAMnD,aAAa,GAAGsC,IAAI,CAACc,MAAM,IAAI,CAACd,IAAI,CAACe,SAAS;IACpD,MAAMtD,MAAM,GAAGuC,IAAI,CAACI,UAAU,CAACD,KAAK,CAAC1C,MAAM;IAC3C,MAAMuD,cAAc,GAAGxE,oBAAoB,CAACkB,aAAa,EAAED,MAAM,CAAC;IAElE,MAAMD,IAAI,GAAGxB,UAAU,CAAC,CAAC;IAEzB,MAAMoB,SAAS,GAAGkB,KAAK,CAAC2C,mBAA6B;IACrD,MAAM5D,MAAM,GAAGP,OAAO,CAACO,MAAM;IAE7B,MAAME,WAAW,GAAGT,OAAO,CAACS,WAAW;IAEvC,MAAM2D,IAAI,GAAG,IAAIf,KAAK,CAACM,QAAQ,EAAE;IAEjC,MAAMU,cAAc,GAAG,GAAGb,OAAO,0BAA0B9C,IAAI,EAAE;IACjE,MAAM4D,cAAc,GAAGpB,IAAI,CAACqB,SAAS;IAErC,MAAMC,aAAa,GAAGC,IAAI,CAACC,KAAK,CAACnE,MAAM,GAAG,GAAG,CAAC;IAC9C,MAAMoE,OAAO,GAAG,MAAMT,cAAc,CAACU,aAAa,CAChDJ,aAAa,EACb/D,WAAW,EACX4D,cAAc,EACd/D,SAAS,EACT;MAAEK,MAAM;MAAEyD;IAAK,CACjB,CAAC;IAED,MAAMS,WAA+B,GAAG;MACtCnE,IAAI;MACJC,MAAM;MACNL,SAAS;MACTC,MAAM;MACNE,WAAW;MACXN,SAAS,EAAEwE,OAAO,CAACxE,SAAS;MAC5BgD,aAAa;MACb2B,SAAS,EAAEpB,UAAU;MACrBqB,UAAU,EAAET,cAAc;MAC1B1D;IACF,CAAC;IAEDoC,OAAO,CAACgC,GAAG,CAACC,GAAG,CAAC,WAAWvE,IAAI,EAAE,EAAEmE,WAAW,CAAC;IAE/C,OAAO5B,CAAC,CAACY,QAAQ,CAACc,OAAO,CAACO,UAAU,CAAC,CAACpB,IAAI,CAAC3E,WAAW,CAAC4E,SAAS,CAAC;EACnE;;EAEA;AACF;AACA;AACA;EACE,MAAMoB,QAAQA,CACZnC,OAA2B,EAC3BoC,SAAuB,EACvBC,OAAoB,EACL;IACf,MAAMlD,YAAY,GAAG,IAAI,CAACZ,wBAAwB,CAAC8D,OAAO,CAAC7D,KAAK,CAAC;IAEjE,IAAI,CAACW,YAAY,EAAE;MACjB,MAAM,IAAI3C,mBAAmB,CAC3B,IAAI,EACJ,kCAAkC,EAClC,IAAI,EACJD,iBAAiB,CAAC+F,iBACpB,CAAC;IACH;IAEA,IAAInD,YAAY,CAACoD,OAAO,EAAExE,MAAM,KAAK,SAAS,EAAE;MAC9C;IACF;IAEA,MAAM;MAAEZ,SAAS;MAAES,aAAa;MAAED;IAAO,CAAC,GAAGwB,YAAY;IACzD,MAAM+B,cAAc,GAAGxE,oBAAoB,CAACkB,aAAa,EAAED,MAAM,CAAC;;IAElE;AACJ;AACA;IACI,MAAMI,MAAM,GAAG,MAAMmD,cAAc,CAACsB,gBAAgB,CAACrF,SAAS,CAAC;IAE/DV,sBAAsB,CAACgG,kBAAkB,CACvC1E,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,CAAC2E,mBAAmB,CAAC1C,OAAO,EAAEb,YAAY,CAAC;MACrD;IACF;IAEA,IAAIpB,MAAM,CAACS,KAAK,CAACT,MAAM,KAAK,YAAY,EAAE;MACxC,MAAM,IAAIvB,mBAAmB,CAC3B,IAAI,EACJ,gFAAgF,EAChF,IAAI,EACJD,iBAAiB,CAACoG,cACpB,CAAC;IACH;IAEA,MAAMC,QAAQ,GAAG,MAAM1B,cAAc,CAAC2B,cAAc,CAClD1F,SAAS,EACTY,MAAM,CAACR,MACT,CAAC;IAED,IAAI,CAACqF,QAAQ,EAAE;MACb,MAAM,IAAIpG,mBAAmB,CAC3B,IAAI,EACJ,qFAAqF,EACrF,KACF,CAAC;IACH;IAEA,MAAM,IAAI,CAACkG,mBAAmB,CAAC1C,OAAO,EAAEb,YAAY,CAAC;EACvD;;EAEA;AACF;AACA;AACA;EACE,MAAcuD,mBAAmBA,CAC/B1C,OAA2B,EAC3Bb,YAA0B,EACX;IACf,MAAM2D,YAA0B,GAAG;MACjC,GAAG3D,YAAY;MACfoD,OAAO,EAAE;QACPxE,MAAM,EAAE,SAAS;QACjBE,SAAS,EAAE,IAAI8E,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,CAACxE,IAAI,GAAGoE;MACf,CAAC,CAAC;IACJ;EACF;AACF;;AAiBA;AACA;AACA","ignoreList":[]}
|
|
@@ -158,12 +158,12 @@ function ItemRepeat(page, state, options) {
|
|
|
158
158
|
title
|
|
159
159
|
} = repeat.options;
|
|
160
160
|
const values = page.getListFromState(state);
|
|
161
|
-
const unit = values.length === 1 ?
|
|
161
|
+
const unit = values.length === 1 ? 'answer' : 'answers';
|
|
162
162
|
return {
|
|
163
163
|
name,
|
|
164
164
|
label: title,
|
|
165
|
-
title
|
|
166
|
-
value: values.length ? `You added ${values.length} ${unit}` : '',
|
|
165
|
+
title,
|
|
166
|
+
value: values.length ? `You have added ${values.length} ${unit}` : '',
|
|
167
167
|
href: getPageHref(page, options.path, {
|
|
168
168
|
returnUrl: getPageHref(page, page.getSummaryPath())
|
|
169
169
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SummaryViewModel.js","names":["SchemaVersion","PaymentField","getAnswer","evaluateTemplate","getError","getPageHref","RepeatPageController","validationOptions","opts","SummaryViewModel","page","pageTitle","declaration","details","checkAnswers","context","name","backLink","feedbackLink","phaseTag","errors","serviceUrl","hasMissingNotificationEmail","components","allowSaveAndExit","paymentState","paymentDetails","constructor","request","model","basePath","def","sections","isForceAccess","title","schema","V2","result","makeFilteredSchema","relevantPages","validate","relevantState","stripUnknown","error","map","summaryDetails","detail","rows","items","item","push","href","text","classes","visuallyHiddenText","label","key","value","html","actions","undefined","summaryList","state","forEach","section","sectionPages","filter","collection","path","ItemRepeat","getSummaryPath","field","fields","ItemField","length","options","repeat","values","getListFromState","unit","returnUrl","subItems","repeatState","required","getFirstError"],"sources":["../../../../../src/server/plugins/engine/models/SummaryViewModel.ts"],"sourcesContent":["import { SchemaVersion, type Section } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type BackLink,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport {\n evaluateTemplate,\n getError,\n getPageHref\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n type Detail,\n type DetailItem,\n type DetailItemField,\n type DetailItemRepeat\n} from '~/src/server/plugins/engine/models/types.js'\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type CheckAnswers,\n type FormContext,\n type FormContextRequest,\n type FormState,\n type FormSubmissionError,\n type SummaryListAction,\n type SummaryListRow\n} from '~/src/server/plugins/engine/types.js'\n\nexport class SummaryViewModel {\n /**\n * Responsible for parsing state values to the govuk-frontend summary list template\n */\n\n page: PageControllerClass\n pageTitle: string\n declaration?: string\n details: Detail[]\n checkAnswers: CheckAnswers[]\n context: FormContext\n name: string | undefined\n backLink?: BackLink\n feedbackLink?: string\n phaseTag?: string\n errors?: FormSubmissionError[]\n serviceUrl: string\n hasMissingNotificationEmail?: boolean\n components?: ComponentViewModel[]\n allowSaveAndExit = false\n paymentState?: PaymentState\n paymentDetails?: CheckAnswers\n\n constructor(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\n ) {\n const { model } = page\n const { basePath, def, sections } = model\n const { isForceAccess } = context\n\n this.page = page\n this.pageTitle = page.title\n if (def.schema === SchemaVersion.V2 && !page.title) {\n this.pageTitle = 'Check your answers before sending your form'\n }\n\n this.serviceUrl = `/${basePath}`\n this.name = def.name\n this.declaration = def.declaration\n this.context = context\n\n const result = model\n .makeFilteredSchema(this.context.relevantPages)\n .validate(this.context.relevantState, { ...opts, stripUnknown: true })\n\n // Format errors\n this.errors = result.error?.details.map(getError)\n this.details = this.summaryDetails(request, sections)\n\n // Format check answers\n this.checkAnswers = this.details.map((detail): CheckAnswers => {\n const { title } = detail\n\n const rows = detail.items.map((item): SummaryListRow => {\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n items.push({\n href: item.href,\n text: 'Change',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: item.label\n })\n }\n\n return {\n key: {\n text: evaluateTemplate(item.title, context)\n },\n value: {\n classes: 'app-prose-scope',\n html: item.value || 'Not provided'\n },\n actions: {\n items\n }\n }\n })\n\n return {\n title: title ? { text: title } : undefined,\n summaryList: { rows }\n }\n })\n }\n\n private summaryDetails(request: FormContextRequest, sections: Section[]) {\n const { context, errors } = this\n const { relevantPages, state } = context\n\n const details: Detail[] = []\n\n ;[...sections, undefined].forEach((section) => {\n const items: DetailItem[] = []\n\n const sectionPages = relevantPages.filter(\n (page) => page.section === section\n )\n\n sectionPages.forEach((page) => {\n const { collection, path } = page\n\n if (page instanceof RepeatPageController) {\n items.push(\n ItemRepeat(page, state, {\n path: page.getSummaryPath(request),\n errors\n })\n )\n } else {\n for (const field of collection.fields) {\n // PaymentField is rendered in its own section, skip it here\n if (field instanceof PaymentField) {\n continue\n }\n items.push(ItemField(page, state, field, { path, errors }))\n }\n }\n })\n\n if (items.length) {\n details.push({\n name: section?.name,\n title: section?.title,\n items\n })\n }\n })\n\n return details\n }\n}\n\n/**\n * Creates a repeater detail item\n * @see {@link DetailItemField}\n */\nfunction ItemRepeat(\n page: RepeatPageController,\n state: FormState,\n options: {\n path: string\n errors?: FormSubmissionError[]\n }\n): DetailItemRepeat {\n const { collection, repeat } = page\n const { name, title } = repeat.options\n\n const values = page.getListFromState(state)\n const unit = values.length === 1 ? title : `${title}s`\n\n return {\n name,\n label: title,\n title: values.length ? `${unit} added` : unit,\n value: values.length ? `You added ${values.length} ${unit}` : '',\n href: getPageHref(page, options.path, {\n returnUrl: getPageHref(page, page.getSummaryPath())\n }),\n state,\n page,\n\n // Repeater field detail items\n subItems: values.map((repeatState) =>\n collection.fields.map((field) =>\n ItemField(page, repeatState, field, options)\n )\n )\n }\n}\n\n/**\n * Creates a form field detail item\n * @see {@link DetailItemField}\n */\nexport function ItemField(\n page: PageControllerClass,\n state: FormState,\n field: Field,\n options: {\n path: string\n errors?: FormSubmissionError[]\n }\n): DetailItemField {\n return {\n name: field.name,\n label: field.title,\n title:\n field.options.required === false\n ? `${field.label} (optional)`\n : field.label,\n error: field.getFirstError(options.errors),\n value: getAnswer(field, state),\n href: getPageHref(page, options.path, {\n returnUrl: getPageHref(page, page.getSummaryPath())\n }),\n state,\n page,\n field\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"SummaryViewModel.js","names":["SchemaVersion","PaymentField","getAnswer","evaluateTemplate","getError","getPageHref","RepeatPageController","validationOptions","opts","SummaryViewModel","page","pageTitle","declaration","details","checkAnswers","context","name","backLink","feedbackLink","phaseTag","errors","serviceUrl","hasMissingNotificationEmail","components","allowSaveAndExit","paymentState","paymentDetails","constructor","request","model","basePath","def","sections","isForceAccess","title","schema","V2","result","makeFilteredSchema","relevantPages","validate","relevantState","stripUnknown","error","map","summaryDetails","detail","rows","items","item","push","href","text","classes","visuallyHiddenText","label","key","value","html","actions","undefined","summaryList","state","forEach","section","sectionPages","filter","collection","path","ItemRepeat","getSummaryPath","field","fields","ItemField","length","options","repeat","values","getListFromState","unit","returnUrl","subItems","repeatState","required","getFirstError"],"sources":["../../../../../src/server/plugins/engine/models/SummaryViewModel.ts"],"sourcesContent":["import { SchemaVersion, type Section } from '@defra/forms-model'\n\nimport { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'\nimport { type PaymentState } from '~/src/server/plugins/engine/components/PaymentField.types.js'\nimport {\n getAnswer,\n type Field\n} from '~/src/server/plugins/engine/components/helpers/components.js'\nimport {\n type BackLink,\n type ComponentViewModel\n} from '~/src/server/plugins/engine/components/types.js'\nimport {\n evaluateTemplate,\n getError,\n getPageHref\n} from '~/src/server/plugins/engine/helpers.js'\nimport {\n type Detail,\n type DetailItem,\n type DetailItemField,\n type DetailItemRepeat\n} from '~/src/server/plugins/engine/models/types.js'\nimport { RepeatPageController } from '~/src/server/plugins/engine/pageControllers/RepeatPageController.js'\nimport { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'\nimport { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type CheckAnswers,\n type FormContext,\n type FormContextRequest,\n type FormState,\n type FormSubmissionError,\n type SummaryListAction,\n type SummaryListRow\n} from '~/src/server/plugins/engine/types.js'\n\nexport class SummaryViewModel {\n /**\n * Responsible for parsing state values to the govuk-frontend summary list template\n */\n\n page: PageControllerClass\n pageTitle: string\n declaration?: string\n details: Detail[]\n checkAnswers: CheckAnswers[]\n context: FormContext\n name: string | undefined\n backLink?: BackLink\n feedbackLink?: string\n phaseTag?: string\n errors?: FormSubmissionError[]\n serviceUrl: string\n hasMissingNotificationEmail?: boolean\n components?: ComponentViewModel[]\n allowSaveAndExit = false\n paymentState?: PaymentState\n paymentDetails?: CheckAnswers\n\n constructor(\n request: FormContextRequest,\n page: PageControllerClass,\n context: FormContext\n ) {\n const { model } = page\n const { basePath, def, sections } = model\n const { isForceAccess } = context\n\n this.page = page\n this.pageTitle = page.title\n if (def.schema === SchemaVersion.V2 && !page.title) {\n this.pageTitle = 'Check your answers before sending your form'\n }\n\n this.serviceUrl = `/${basePath}`\n this.name = def.name\n this.declaration = def.declaration\n this.context = context\n\n const result = model\n .makeFilteredSchema(this.context.relevantPages)\n .validate(this.context.relevantState, { ...opts, stripUnknown: true })\n\n // Format errors\n this.errors = result.error?.details.map(getError)\n this.details = this.summaryDetails(request, sections)\n\n // Format check answers\n this.checkAnswers = this.details.map((detail): CheckAnswers => {\n const { title } = detail\n\n const rows = detail.items.map((item): SummaryListRow => {\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n items.push({\n href: item.href,\n text: 'Change',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: item.label\n })\n }\n\n return {\n key: {\n text: evaluateTemplate(item.title, context)\n },\n value: {\n classes: 'app-prose-scope',\n html: item.value || 'Not provided'\n },\n actions: {\n items\n }\n }\n })\n\n return {\n title: title ? { text: title } : undefined,\n summaryList: { rows }\n }\n })\n }\n\n private summaryDetails(request: FormContextRequest, sections: Section[]) {\n const { context, errors } = this\n const { relevantPages, state } = context\n\n const details: Detail[] = []\n\n ;[...sections, undefined].forEach((section) => {\n const items: DetailItem[] = []\n\n const sectionPages = relevantPages.filter(\n (page) => page.section === section\n )\n\n sectionPages.forEach((page) => {\n const { collection, path } = page\n\n if (page instanceof RepeatPageController) {\n items.push(\n ItemRepeat(page, state, {\n path: page.getSummaryPath(request),\n errors\n })\n )\n } else {\n for (const field of collection.fields) {\n // PaymentField is rendered in its own section, skip it here\n if (field instanceof PaymentField) {\n continue\n }\n items.push(ItemField(page, state, field, { path, errors }))\n }\n }\n })\n\n if (items.length) {\n details.push({\n name: section?.name,\n title: section?.title,\n items\n })\n }\n })\n\n return details\n }\n}\n\n/**\n * Creates a repeater detail item\n * @see {@link DetailItemField}\n */\nfunction ItemRepeat(\n page: RepeatPageController,\n state: FormState,\n options: {\n path: string\n errors?: FormSubmissionError[]\n }\n): DetailItemRepeat {\n const { collection, repeat } = page\n const { name, title } = repeat.options\n\n const values = page.getListFromState(state)\n const unit = values.length === 1 ? 'answer' : 'answers'\n\n return {\n name,\n label: title,\n title,\n value: values.length ? `You have added ${values.length} ${unit}` : '',\n href: getPageHref(page, options.path, {\n returnUrl: getPageHref(page, page.getSummaryPath())\n }),\n state,\n page,\n\n // Repeater field detail items\n subItems: values.map((repeatState) =>\n collection.fields.map((field) =>\n ItemField(page, repeatState, field, options)\n )\n )\n }\n}\n\n/**\n * Creates a form field detail item\n * @see {@link DetailItemField}\n */\nexport function ItemField(\n page: PageControllerClass,\n state: FormState,\n field: Field,\n options: {\n path: string\n errors?: FormSubmissionError[]\n }\n): DetailItemField {\n return {\n name: field.name,\n label: field.title,\n title:\n field.options.required === false\n ? `${field.label} (optional)`\n : field.label,\n error: field.getFirstError(options.errors),\n value: getAnswer(field, state),\n href: getPageHref(page, options.path, {\n returnUrl: getPageHref(page, page.getSummaryPath())\n }),\n state,\n page,\n field\n }\n}\n"],"mappings":"AAAA,SAASA,aAAa,QAAsB,oBAAoB;AAEhE,SAASC,YAAY;AAErB,SACEC,SAAS;AAOX,SACEC,gBAAgB,EAChBC,QAAQ,EACRC,WAAW;AAQb,SAASC,oBAAoB;AAE7B,SAASC,iBAAiB,IAAIC,IAAI;AAWlC,OAAO,MAAMC,gBAAgB,CAAC;EAC5B;AACF;AACA;;EAEEC,IAAI;EACJC,SAAS;EACTC,WAAW;EACXC,OAAO;EACPC,YAAY;EACZC,OAAO;EACPC,IAAI;EACJC,QAAQ;EACRC,YAAY;EACZC,QAAQ;EACRC,MAAM;EACNC,UAAU;EACVC,2BAA2B;EAC3BC,UAAU;EACVC,gBAAgB,GAAG,KAAK;EACxBC,YAAY;EACZC,cAAc;EAEdC,WAAWA,CACTC,OAA2B,EAC3BlB,IAAyB,EACzBK,OAAoB,EACpB;IACA,MAAM;MAAEc;IAAM,CAAC,GAAGnB,IAAI;IACtB,MAAM;MAAEoB,QAAQ;MAAEC,GAAG;MAAEC;IAAS,CAAC,GAAGH,KAAK;IACzC,MAAM;MAAEI;IAAc,CAAC,GAAGlB,OAAO;IAEjC,IAAI,CAACL,IAAI,GAAGA,IAAI;IAChB,IAAI,CAACC,SAAS,GAAGD,IAAI,CAACwB,KAAK;IAC3B,IAAIH,GAAG,CAACI,MAAM,KAAKnC,aAAa,CAACoC,EAAE,IAAI,CAAC1B,IAAI,CAACwB,KAAK,EAAE;MAClD,IAAI,CAACvB,SAAS,GAAG,6CAA6C;IAChE;IAEA,IAAI,CAACU,UAAU,GAAG,IAAIS,QAAQ,EAAE;IAChC,IAAI,CAACd,IAAI,GAAGe,GAAG,CAACf,IAAI;IACpB,IAAI,CAACJ,WAAW,GAAGmB,GAAG,CAACnB,WAAW;IAClC,IAAI,CAACG,OAAO,GAAGA,OAAO;IAEtB,MAAMsB,MAAM,GAAGR,KAAK,CACjBS,kBAAkB,CAAC,IAAI,CAACvB,OAAO,CAACwB,aAAa,CAAC,CAC9CC,QAAQ,CAAC,IAAI,CAACzB,OAAO,CAAC0B,aAAa,EAAE;MAAE,GAAGjC,IAAI;MAAEkC,YAAY,EAAE;IAAK,CAAC,CAAC;;IAExE;IACA,IAAI,CAACtB,MAAM,GAAGiB,MAAM,CAACM,KAAK,EAAE9B,OAAO,CAAC+B,GAAG,CAACxC,QAAQ,CAAC;IACjD,IAAI,CAACS,OAAO,GAAG,IAAI,CAACgC,cAAc,CAACjB,OAAO,EAAEI,QAAQ,CAAC;;IAErD;IACA,IAAI,CAAClB,YAAY,GAAG,IAAI,CAACD,OAAO,CAAC+B,GAAG,CAAEE,MAAM,IAAmB;MAC7D,MAAM;QAAEZ;MAAM,CAAC,GAAGY,MAAM;MAExB,MAAMC,IAAI,GAAGD,MAAM,CAACE,KAAK,CAACJ,GAAG,CAAEK,IAAI,IAAqB;QACtD,MAAMD,KAA0B,GAAG,EAAE;;QAErC;QACA,IAAI,CAACf,aAAa,EAAE;UAClBe,KAAK,CAACE,IAAI,CAAC;YACTC,IAAI,EAAEF,IAAI,CAACE,IAAI;YACfC,IAAI,EAAE,QAAQ;YACdC,OAAO,EAAE,8BAA8B;YACvCC,kBAAkB,EAAEL,IAAI,CAACM;UAC3B,CAAC,CAAC;QACJ;QAEA,OAAO;UACLC,GAAG,EAAE;YACHJ,IAAI,EAAEjD,gBAAgB,CAAC8C,IAAI,CAACf,KAAK,EAAEnB,OAAO;UAC5C,CAAC;UACD0C,KAAK,EAAE;YACLJ,OAAO,EAAE,iBAAiB;YAC1BK,IAAI,EAAET,IAAI,CAACQ,KAAK,IAAI;UACtB,CAAC;UACDE,OAAO,EAAE;YACPX;UACF;QACF,CAAC;MACH,CAAC,CAAC;MAEF,OAAO;QACLd,KAAK,EAAEA,KAAK,GAAG;UAAEkB,IAAI,EAAElB;QAAM,CAAC,GAAG0B,SAAS;QAC1CC,WAAW,EAAE;UAAEd;QAAK;MACtB,CAAC;IACH,CAAC,CAAC;EACJ;EAEQF,cAAcA,CAACjB,OAA2B,EAAEI,QAAmB,EAAE;IACvE,MAAM;MAAEjB,OAAO;MAAEK;IAAO,CAAC,GAAG,IAAI;IAChC,MAAM;MAAEmB,aAAa;MAAEuB;IAAM,CAAC,GAAG/C,OAAO;IAExC,MAAMF,OAAiB,GAAG,EAAE;IAE3B,CAAC,GAAGmB,QAAQ,EAAE4B,SAAS,CAAC,CAACG,OAAO,CAAEC,OAAO,IAAK;MAC7C,MAAMhB,KAAmB,GAAG,EAAE;MAE9B,MAAMiB,YAAY,GAAG1B,aAAa,CAAC2B,MAAM,CACtCxD,IAAI,IAAKA,IAAI,CAACsD,OAAO,KAAKA,OAC7B,CAAC;MAEDC,YAAY,CAACF,OAAO,CAAErD,IAAI,IAAK;QAC7B,MAAM;UAAEyD,UAAU;UAAEC;QAAK,CAAC,GAAG1D,IAAI;QAEjC,IAAIA,IAAI,YAAYJ,oBAAoB,EAAE;UACxC0C,KAAK,CAACE,IAAI,CACRmB,UAAU,CAAC3D,IAAI,EAAEoD,KAAK,EAAE;YACtBM,IAAI,EAAE1D,IAAI,CAAC4D,cAAc,CAAC1C,OAAO,CAAC;YAClCR;UACF,CAAC,CACH,CAAC;QACH,CAAC,MAAM;UACL,KAAK,MAAMmD,KAAK,IAAIJ,UAAU,CAACK,MAAM,EAAE;YACrC;YACA,IAAID,KAAK,YAAYtE,YAAY,EAAE;cACjC;YACF;YACA+C,KAAK,CAACE,IAAI,CAACuB,SAAS,CAAC/D,IAAI,EAAEoD,KAAK,EAAES,KAAK,EAAE;cAAEH,IAAI;cAAEhD;YAAO,CAAC,CAAC,CAAC;UAC7D;QACF;MACF,CAAC,CAAC;MAEF,IAAI4B,KAAK,CAAC0B,MAAM,EAAE;QAChB7D,OAAO,CAACqC,IAAI,CAAC;UACXlC,IAAI,EAAEgD,OAAO,EAAEhD,IAAI;UACnBkB,KAAK,EAAE8B,OAAO,EAAE9B,KAAK;UACrBc;QACF,CAAC,CAAC;MACJ;IACF,CAAC,CAAC;IAEF,OAAOnC,OAAO;EAChB;AACF;;AAEA;AACA;AACA;AACA;AACA,SAASwD,UAAUA,CACjB3D,IAA0B,EAC1BoD,KAAgB,EAChBa,OAGC,EACiB;EAClB,MAAM;IAAER,UAAU;IAAES;EAAO,CAAC,GAAGlE,IAAI;EACnC,MAAM;IAAEM,IAAI;IAAEkB;EAAM,CAAC,GAAG0C,MAAM,CAACD,OAAO;EAEtC,MAAME,MAAM,GAAGnE,IAAI,CAACoE,gBAAgB,CAAChB,KAAK,CAAC;EAC3C,MAAMiB,IAAI,GAAGF,MAAM,CAACH,MAAM,KAAK,CAAC,GAAG,QAAQ,GAAG,SAAS;EAEvD,OAAO;IACL1D,IAAI;IACJuC,KAAK,EAAErB,KAAK;IACZA,KAAK;IACLuB,KAAK,EAAEoB,MAAM,CAACH,MAAM,GAAG,kBAAkBG,MAAM,CAACH,MAAM,IAAIK,IAAI,EAAE,GAAG,EAAE;IACrE5B,IAAI,EAAE9C,WAAW,CAACK,IAAI,EAAEiE,OAAO,CAACP,IAAI,EAAE;MACpCY,SAAS,EAAE3E,WAAW,CAACK,IAAI,EAAEA,IAAI,CAAC4D,cAAc,CAAC,CAAC;IACpD,CAAC,CAAC;IACFR,KAAK;IACLpD,IAAI;IAEJ;IACAuE,QAAQ,EAAEJ,MAAM,CAACjC,GAAG,CAAEsC,WAAW,IAC/Bf,UAAU,CAACK,MAAM,CAAC5B,GAAG,CAAE2B,KAAK,IAC1BE,SAAS,CAAC/D,IAAI,EAAEwE,WAAW,EAAEX,KAAK,EAAEI,OAAO,CAC7C,CACF;EACF,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASF,SAASA,CACvB/D,IAAyB,EACzBoD,KAAgB,EAChBS,KAAY,EACZI,OAGC,EACgB;EACjB,OAAO;IACL3D,IAAI,EAAEuD,KAAK,CAACvD,IAAI;IAChBuC,KAAK,EAAEgB,KAAK,CAACrC,KAAK;IAClBA,KAAK,EACHqC,KAAK,CAACI,OAAO,CAACQ,QAAQ,KAAK,KAAK,GAC5B,GAAGZ,KAAK,CAAChB,KAAK,aAAa,GAC3BgB,KAAK,CAAChB,KAAK;IACjBZ,KAAK,EAAE4B,KAAK,CAACa,aAAa,CAACT,OAAO,CAACvD,MAAM,CAAC;IAC1CqC,KAAK,EAAEvD,SAAS,CAACqE,KAAK,EAAET,KAAK,CAAC;IAC9BX,IAAI,EAAE9C,WAAW,CAACK,IAAI,EAAEiE,OAAO,CAACP,IAAI,EAAE;MACpCY,SAAS,EAAE3E,WAAW,CAACK,IAAI,EAAEA,IAAI,CAAC4D,cAAc,CAAC,CAAC;IACpD,CAAC,CAAC;IACFR,KAAK;IACLpD,IAAI;IACJ6D;EACF,CAAC;AACH","ignoreList":[]}
|
|
@@ -5,7 +5,7 @@ exports[`SummaryViewModel Check answers (0 items) should use correct summary lab
|
|
|
5
5
|
{
|
|
6
6
|
"label": "Pizza",
|
|
7
7
|
"name": "pizza",
|
|
8
|
-
"title": "
|
|
8
|
+
"title": "Pizza",
|
|
9
9
|
"value": "",
|
|
10
10
|
},
|
|
11
11
|
{
|
|
@@ -22,8 +22,8 @@ exports[`SummaryViewModel Check answers (1 item) should use correct summary labe
|
|
|
22
22
|
{
|
|
23
23
|
"label": "Pizza",
|
|
24
24
|
"name": "pizza",
|
|
25
|
-
"title": "Pizza
|
|
26
|
-
"value": "You added 1
|
|
25
|
+
"title": "Pizza",
|
|
26
|
+
"value": "You have added 1 answer",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"label": "How would you like to receive your pizza?",
|
|
@@ -39,8 +39,8 @@ exports[`SummaryViewModel Check answers (2 items) should use correct summary lab
|
|
|
39
39
|
{
|
|
40
40
|
"label": "Pizza",
|
|
41
41
|
"name": "pizza",
|
|
42
|
-
"title": "
|
|
43
|
-
"value": "You added 2
|
|
42
|
+
"title": "Pizza",
|
|
43
|
+
"value": "You have added 2 answers",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"label": "How would you like to receive your pizza?",
|
|
@@ -161,8 +161,7 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
161
161
|
query
|
|
162
162
|
} = request;
|
|
163
163
|
const {
|
|
164
|
-
schema
|
|
165
|
-
options
|
|
164
|
+
schema
|
|
166
165
|
} = repeat;
|
|
167
166
|
const {
|
|
168
167
|
state
|
|
@@ -183,7 +182,7 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
183
182
|
// Show error if repeat limits apply
|
|
184
183
|
if (hasErrorMin || hasErrorMax) {
|
|
185
184
|
const count = hasErrorMax ? schema.max : schema.min;
|
|
186
|
-
const itemTitle =
|
|
185
|
+
const itemTitle = `answer${count === 1 ? '' : 's'}`;
|
|
187
186
|
context.errors = [{
|
|
188
187
|
path: [],
|
|
189
188
|
href: '',
|
|
@@ -229,10 +228,10 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
229
228
|
...viewModel,
|
|
230
229
|
context,
|
|
231
230
|
backLink: this.getBackLink(request, context),
|
|
232
|
-
pageTitle:
|
|
231
|
+
pageTitle: 'Are you sure you want to remove this answer?',
|
|
233
232
|
itemTitle: `${title} ${list.indexOf(item) + 1}`,
|
|
234
233
|
buttonConfirm: {
|
|
235
|
-
text:
|
|
234
|
+
text: 'Remove'
|
|
236
235
|
},
|
|
237
236
|
buttonCancel: {
|
|
238
237
|
text: 'Cancel'
|
|
@@ -346,11 +345,12 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
346
345
|
});
|
|
347
346
|
});
|
|
348
347
|
}
|
|
348
|
+
const unit = count === 1 ? 'answer' : 'answers';
|
|
349
349
|
return {
|
|
350
350
|
...this.viewModel,
|
|
351
351
|
backLink: this.getBackLink(request, context),
|
|
352
352
|
repeatTitle: title,
|
|
353
|
-
pageTitle: `You have added ${count} ${
|
|
353
|
+
pageTitle: `You have added ${count} ${unit}`,
|
|
354
354
|
showTitle: true,
|
|
355
355
|
context,
|
|
356
356
|
errors,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RepeatPageController.js","names":["randomUUID","Boom","Joi","isRepeatState","redirectPath","QuestionPageController","FormAction","RepeatPageController","listSummaryViewName","listDeleteViewName","repeat","allowSaveAndExit","constructor","model","pageDef","options","schema","itemId","string","uuid","required","collection","formSchema","append","stateSchema","object","keys","name","array","items","min","max","label","title","getFormParams","request","params","payload","getFormDataFromState","state","list","getListFromState","getItemId","item","getItemFromList","getStateFromValidForm","badRequest","itemState","updated","newList","push","indexOf","proceed","h","nextPath","getSummaryPath","find","values","makeGetRouteHandler","context","path","query","summaryPath","returnUrl","force","length","makeGetListSummaryRouteHandler","viewModel","getListSummaryViewModel","view","makePostListSummaryRouteHandler","action","hasErrorMin","Continue","hasErrorMax","AddAnother","count","itemTitle","errors","href","text","SaveAndExit","handleSaveAndExit","getNextPath","makeGetItemDeleteRouteHandler","notFound","backLink","getBackLink","pageTitle","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","splice","update","mergeState","getViewModel","itemNumber","repeatCaption","sectionTitle","isForceAccess","summaryList","classes","rows","Array","isArray","forEach","index","getHref","visuallyHiddenText","itemDisplayText","fields","getDisplayStringFromState","key","value","actions","repeatTitle","showTitle","checkAnswers","shouldShowSaveAndExit","server"],"sources":["../../../../../src/server/plugins/engine/pageControllers/RepeatPageController.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\n\nimport { type PageRepeat, type Repeat } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport Joi from 'joi'\n\nimport { isRepeatState } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { redirectPath } 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 {\n type FormContext,\n type FormContextRequest,\n type FormPageViewModel,\n type FormPayload,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type RepeatItemState,\n type RepeatListState,\n type RepeaterSummaryPageViewModel,\n type SummaryList,\n type SummaryListAction\n} from '~/src/server/plugins/engine/types.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class RepeatPageController extends QuestionPageController {\n declare pageDef: PageRepeat\n\n listSummaryViewName = 'repeat-list-summary'\n listDeleteViewName = 'item-delete'\n repeat: Repeat\n allowSaveAndExit = true\n\n constructor(model: FormModel, pageDef: PageRepeat) {\n super(model, pageDef)\n\n this.repeat = pageDef.repeat\n\n const { options, schema } = this.repeat\n const itemId = Joi.string().uuid().required()\n\n this.collection.formSchema = this.collection.formSchema.append({ itemId })\n this.collection.stateSchema = Joi.object<RepeatItemState>().keys({\n [options.name]: Joi.array()\n .items(this.collection.stateSchema.append({ itemId }))\n .min(schema.min)\n .max(schema.max)\n .label(`${options.title} list`)\n .required()\n })\n }\n\n get keys() {\n const { repeat } = this\n return [repeat.options.name, ...super.keys]\n }\n\n getFormParams(request?: FormContextRequest) {\n const params = super.getFormParams(request)\n\n // Apply an itemId to the form payload\n if (request?.payload) {\n params.itemId = request.params.itemId ?? randomUUID()\n }\n\n return params\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { repeat } = this\n\n const params = this.getFormParams(request)\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n\n // Create payload with repeater list state\n if (!itemId) {\n return {\n ...params,\n [repeat.options.name]: list\n }\n }\n\n // Create payload with repeater item state\n const item = this.getItemFromList(list, itemId)\n\n return {\n ...params,\n ...item\n }\n }\n\n getStateFromValidForm(\n request: FormContextRequest,\n state: FormSubmissionState,\n payload: FormPayload\n ) {\n const itemId = this.getItemId(request)\n\n if (!itemId) {\n throw Boom.badRequest('No item ID found')\n }\n\n const list = this.getListFromState(state)\n const item = this.getItemFromList(list, itemId)\n\n const itemState = super.getStateFromValidForm(request, state, payload)\n const updated: RepeatItemState = { ...itemState, itemId }\n const newList = [...list]\n\n if (!item) {\n // Adding a new item\n newList.push(updated)\n } else {\n // Update an existing item\n newList[list.indexOf(item)] = updated\n }\n\n return {\n [this.repeat.options.name]: newList\n }\n }\n\n proceed(request: FormContextRequest, h: FormResponseToolkit) {\n const nextPath = this.getSummaryPath(request)\n return super.proceed(request, h, nextPath)\n }\n\n getItemFromList(list: RepeatListState, itemId?: string) {\n return list.find((item) => item.itemId === itemId)\n }\n\n getListFromState(state: FormSubmissionState) {\n const { name } = this.repeat.options\n const values = state[name]\n\n return isRepeatState(values) ? values : []\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { query } = request\n const { state } = context\n\n const itemId = this.getItemId(request)\n const list = this.getListFromState(state)\n\n if (!itemId) {\n const summaryPath = this.getSummaryPath(request)\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl,\n force: query.force\n })\n\n // Only redirect to new item when list is empty\n return super.proceed(request, h, list.length ? summaryPath : nextPath)\n }\n\n return super.makeGetRouteHandler()(request, context, h)\n }\n }\n\n makeGetListSummaryRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { query } = request\n const { state } = context\n\n const list = this.getListFromState(state)\n\n if (!list.length) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n const viewModel = this.getListSummaryViewModel(request, context, list)\n\n return h.view(this.listSummaryViewName, viewModel)\n }\n }\n\n makePostListSummaryRouteHandler() {\n return (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path, repeat } = this\n const { query } = request\n const { schema, options } = repeat\n const { state } = context\n\n const list = this.getListFromState(state)\n\n if (!list.length) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n const { action } = this.getFormParams(request)\n\n const hasErrorMin =\n action === FormAction.Continue && list.length < schema.min\n\n const hasErrorMax =\n (action === FormAction.AddAnother && list.length >= schema.max) ||\n (action === FormAction.Continue && list.length > schema.max)\n\n // Show error if repeat limits apply\n if (hasErrorMin || hasErrorMax) {\n const count = hasErrorMax ? schema.max : schema.min\n const itemTitle = `${options.title}${count === 1 ? '' : 's'}`\n\n context.errors = [\n {\n path: [],\n href: '',\n name: '',\n text: hasErrorMax\n ? `You can only add up to ${count} ${itemTitle}`\n : `You must add at least ${count} ${itemTitle}`\n }\n ]\n\n const viewModel = this.getListSummaryViewModel(request, context, list)\n\n return h.view(this.listSummaryViewName, viewModel)\n }\n\n if (action === FormAction.AddAnother) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n // Check if this is a save-and-exit action\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n const nextPath = this.getNextPath(context)\n return super.proceed(request, h, nextPath)\n }\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { state } = context\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n if (!item || list.length === 1) {\n throw Boom.notFound(\n item\n ? 'Last list item cannot be removed'\n : 'List item to remove not found'\n )\n }\n\n const { title } = this.repeat.options\n\n return h.view(this.listDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: `Are you sure you want to remove this ${title}?`,\n itemTitle: `${title} ${list.indexOf(item) + 1}`,\n buttonConfirm: { text: `Remove ${title}` },\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 { repeat } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n if (!item || list.length === 1) {\n throw Boom.notFound(\n item\n ? 'Last list item cannot be removed'\n : 'List item to remove not found'\n )\n }\n\n // Remove the item from the list\n if (confirm) {\n list.splice(list.indexOf(item), 1)\n\n const update = {\n [repeat.options.name]: list\n }\n\n await this.mergeState(request, state, update)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FormPageViewModel {\n const { state } = context\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n const viewModel = super.getViewModel(request, context)\n const itemNumber = item ? list.indexOf(item) + 1 : list.length + 1\n const repeatCaption = `${this.repeat.options.title} ${itemNumber}`\n\n return {\n ...viewModel,\n\n sectionTitle: viewModel.sectionTitle\n ? `${viewModel.sectionTitle}: ${repeatCaption}`\n : repeatCaption\n }\n }\n\n getListSummaryViewModel(\n request: FormContextRequest,\n context: FormContext,\n list: RepeatListState\n ): RepeaterSummaryPageViewModel {\n const { collection, href, repeat } = this\n const { query } = request\n const { isForceAccess, errors } = context\n\n const { title } = repeat.options\n\n const summaryList: SummaryList = {\n classes: 'govuk-summary-list--long-actions',\n rows: []\n }\n\n let count = 0\n\n if (Array.isArray(list)) {\n count = list.length\n\n const summaryPath = this.getSummaryPath(request)\n\n list.forEach((item, index) => {\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n items.push({\n href: redirectPath(`${href}/${item.itemId}`, {\n returnUrl: query.returnUrl ?? this.getHref(summaryPath)\n }),\n text: 'Change',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: `item ${index + 1}`\n })\n\n if (count > 1) {\n items.push({\n href: redirectPath(`${href}/${item.itemId}/confirm-delete`, {\n returnUrl: query.returnUrl\n }),\n text: 'Remove',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: `item ${index + 1}`\n })\n }\n }\n\n const itemDisplayText = collection.fields.length\n ? collection.fields[0].getDisplayStringFromState(item)\n : ''\n\n summaryList.rows.push({\n key: {\n text: `${title} ${index + 1}`\n },\n value: {\n text: itemDisplayText || 'Not provided'\n },\n actions: {\n items\n }\n })\n })\n }\n\n return {\n ...this.viewModel,\n backLink: this.getBackLink(request, context),\n repeatTitle: title,\n pageTitle: `You have added ${count} ${title}${count === 1 ? '' : 's'}`,\n showTitle: true,\n context,\n errors,\n checkAnswers: [{ summaryList }],\n allowSaveAndExit: this.shouldShowSaveAndExit(request.server)\n }\n }\n\n getSummaryPath(request?: FormContextRequest) {\n const { path } = this\n\n const summaryPath = super.getSummaryPath()\n\n if (!request) {\n return summaryPath\n }\n\n const { query } = request\n\n return redirectPath(`${path}${summaryPath}`, {\n returnUrl: query.returnUrl\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"RepeatPageController.js","names":["randomUUID","Boom","Joi","isRepeatState","redirectPath","QuestionPageController","FormAction","RepeatPageController","listSummaryViewName","listDeleteViewName","repeat","allowSaveAndExit","constructor","model","pageDef","options","schema","itemId","string","uuid","required","collection","formSchema","append","stateSchema","object","keys","name","array","items","min","max","label","title","getFormParams","request","params","payload","getFormDataFromState","state","list","getListFromState","getItemId","item","getItemFromList","getStateFromValidForm","badRequest","itemState","updated","newList","push","indexOf","proceed","h","nextPath","getSummaryPath","find","values","makeGetRouteHandler","context","path","query","summaryPath","returnUrl","force","length","makeGetListSummaryRouteHandler","viewModel","getListSummaryViewModel","view","makePostListSummaryRouteHandler","action","hasErrorMin","Continue","hasErrorMax","AddAnother","count","itemTitle","errors","href","text","SaveAndExit","handleSaveAndExit","getNextPath","makeGetItemDeleteRouteHandler","notFound","backLink","getBackLink","pageTitle","buttonConfirm","buttonCancel","makePostItemDeleteRouteHandler","confirm","splice","update","mergeState","getViewModel","itemNumber","repeatCaption","sectionTitle","isForceAccess","summaryList","classes","rows","Array","isArray","forEach","index","getHref","visuallyHiddenText","itemDisplayText","fields","getDisplayStringFromState","key","value","actions","unit","repeatTitle","showTitle","checkAnswers","shouldShowSaveAndExit","server"],"sources":["../../../../../src/server/plugins/engine/pageControllers/RepeatPageController.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\n\nimport { type PageRepeat, type Repeat } from '@defra/forms-model'\nimport Boom from '@hapi/boom'\nimport Joi from 'joi'\n\nimport { isRepeatState } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { redirectPath } 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 {\n type FormContext,\n type FormContextRequest,\n type FormPageViewModel,\n type FormPayload,\n type FormSubmissionState,\n type ItemDeletePageViewModel,\n type RepeatItemState,\n type RepeatListState,\n type RepeaterSummaryPageViewModel,\n type SummaryList,\n type SummaryListAction\n} from '~/src/server/plugins/engine/types.js'\nimport {\n FormAction,\n type FormRequest,\n type FormRequestPayload,\n type FormResponseToolkit\n} from '~/src/server/routes/types.js'\n\nexport class RepeatPageController extends QuestionPageController {\n declare pageDef: PageRepeat\n\n listSummaryViewName = 'repeat-list-summary'\n listDeleteViewName = 'item-delete'\n repeat: Repeat\n allowSaveAndExit = true\n\n constructor(model: FormModel, pageDef: PageRepeat) {\n super(model, pageDef)\n\n this.repeat = pageDef.repeat\n\n const { options, schema } = this.repeat\n const itemId = Joi.string().uuid().required()\n\n this.collection.formSchema = this.collection.formSchema.append({ itemId })\n this.collection.stateSchema = Joi.object<RepeatItemState>().keys({\n [options.name]: Joi.array()\n .items(this.collection.stateSchema.append({ itemId }))\n .min(schema.min)\n .max(schema.max)\n .label(`${options.title} list`)\n .required()\n })\n }\n\n get keys() {\n const { repeat } = this\n return [repeat.options.name, ...super.keys]\n }\n\n getFormParams(request?: FormContextRequest) {\n const params = super.getFormParams(request)\n\n // Apply an itemId to the form payload\n if (request?.payload) {\n params.itemId = request.params.itemId ?? randomUUID()\n }\n\n return params\n }\n\n getFormDataFromState(\n request: FormContextRequest | undefined,\n state: FormSubmissionState\n ) {\n const { repeat } = this\n\n const params = this.getFormParams(request)\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n\n // Create payload with repeater list state\n if (!itemId) {\n return {\n ...params,\n [repeat.options.name]: list\n }\n }\n\n // Create payload with repeater item state\n const item = this.getItemFromList(list, itemId)\n\n return {\n ...params,\n ...item\n }\n }\n\n getStateFromValidForm(\n request: FormContextRequest,\n state: FormSubmissionState,\n payload: FormPayload\n ) {\n const itemId = this.getItemId(request)\n\n if (!itemId) {\n throw Boom.badRequest('No item ID found')\n }\n\n const list = this.getListFromState(state)\n const item = this.getItemFromList(list, itemId)\n\n const itemState = super.getStateFromValidForm(request, state, payload)\n const updated: RepeatItemState = { ...itemState, itemId }\n const newList = [...list]\n\n if (!item) {\n // Adding a new item\n newList.push(updated)\n } else {\n // Update an existing item\n newList[list.indexOf(item)] = updated\n }\n\n return {\n [this.repeat.options.name]: newList\n }\n }\n\n proceed(request: FormContextRequest, h: FormResponseToolkit) {\n const nextPath = this.getSummaryPath(request)\n return super.proceed(request, h, nextPath)\n }\n\n getItemFromList(list: RepeatListState, itemId?: string) {\n return list.find((item) => item.itemId === itemId)\n }\n\n getListFromState(state: FormSubmissionState) {\n const { name } = this.repeat.options\n const values = state[name]\n\n return isRepeatState(values) ? values : []\n }\n\n makeGetRouteHandler() {\n return async (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { query } = request\n const { state } = context\n\n const itemId = this.getItemId(request)\n const list = this.getListFromState(state)\n\n if (!itemId) {\n const summaryPath = this.getSummaryPath(request)\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl,\n force: query.force\n })\n\n // Only redirect to new item when list is empty\n return super.proceed(request, h, list.length ? summaryPath : nextPath)\n }\n\n return super.makeGetRouteHandler()(request, context, h)\n }\n }\n\n makeGetListSummaryRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path } = this\n const { query } = request\n const { state } = context\n\n const list = this.getListFromState(state)\n\n if (!list.length) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n const viewModel = this.getListSummaryViewModel(request, context, list)\n\n return h.view(this.listSummaryViewName, viewModel)\n }\n }\n\n makePostListSummaryRouteHandler() {\n return (\n request: FormRequestPayload,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { path, repeat } = this\n const { query } = request\n const { schema } = repeat\n const { state } = context\n\n const list = this.getListFromState(state)\n\n if (!list.length) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n const { action } = this.getFormParams(request)\n\n const hasErrorMin =\n action === FormAction.Continue && list.length < schema.min\n\n const hasErrorMax =\n (action === FormAction.AddAnother && list.length >= schema.max) ||\n (action === FormAction.Continue && list.length > schema.max)\n\n // Show error if repeat limits apply\n if (hasErrorMin || hasErrorMax) {\n const count = hasErrorMax ? schema.max : schema.min\n const itemTitle = `answer${count === 1 ? '' : 's'}`\n\n context.errors = [\n {\n path: [],\n href: '',\n name: '',\n text: hasErrorMax\n ? `You can only add up to ${count} ${itemTitle}`\n : `You must add at least ${count} ${itemTitle}`\n }\n ]\n\n const viewModel = this.getListSummaryViewModel(request, context, list)\n\n return h.view(this.listSummaryViewName, viewModel)\n }\n\n if (action === FormAction.AddAnother) {\n const nextPath = redirectPath(`${path}/${randomUUID()}`, {\n returnUrl: query.returnUrl\n })\n\n return super.proceed(request, h, nextPath)\n }\n\n // Check if this is a save-and-exit action\n if (action === FormAction.SaveAndExit) {\n return this.handleSaveAndExit(request, context, h)\n }\n\n const nextPath = this.getNextPath(context)\n return super.proceed(request, h, nextPath)\n }\n }\n\n makeGetItemDeleteRouteHandler() {\n return (\n request: FormRequest,\n context: FormContext,\n h: FormResponseToolkit\n ) => {\n const { viewModel } = this\n const { state } = context\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n if (!item || list.length === 1) {\n throw Boom.notFound(\n item\n ? 'Last list item cannot be removed'\n : 'List item to remove not found'\n )\n }\n\n const { title } = this.repeat.options\n\n return h.view(this.listDeleteViewName, {\n ...viewModel,\n context,\n backLink: this.getBackLink(request, context),\n pageTitle: 'Are you sure you want to remove this answer?',\n itemTitle: `${title} ${list.indexOf(item) + 1}`,\n buttonConfirm: { text: 'Remove' },\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 { repeat } = this\n const { state } = context\n\n const { confirm } = this.getFormParams(request)\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n if (!item || list.length === 1) {\n throw Boom.notFound(\n item\n ? 'Last list item cannot be removed'\n : 'List item to remove not found'\n )\n }\n\n // Remove the item from the list\n if (confirm) {\n list.splice(list.indexOf(item), 1)\n\n const update = {\n [repeat.options.name]: list\n }\n\n await this.mergeState(request, state, update)\n }\n\n return this.proceed(request, h)\n }\n }\n\n getViewModel(\n request: FormContextRequest,\n context: FormContext\n ): FormPageViewModel {\n const { state } = context\n\n const list = this.getListFromState(state)\n const itemId = this.getItemId(request)\n const item = this.getItemFromList(list, itemId)\n\n const viewModel = super.getViewModel(request, context)\n const itemNumber = item ? list.indexOf(item) + 1 : list.length + 1\n const repeatCaption = `${this.repeat.options.title} ${itemNumber}`\n\n return {\n ...viewModel,\n\n sectionTitle: viewModel.sectionTitle\n ? `${viewModel.sectionTitle}: ${repeatCaption}`\n : repeatCaption\n }\n }\n\n getListSummaryViewModel(\n request: FormContextRequest,\n context: FormContext,\n list: RepeatListState\n ): RepeaterSummaryPageViewModel {\n const { collection, href, repeat } = this\n const { query } = request\n const { isForceAccess, errors } = context\n\n const { title } = repeat.options\n\n const summaryList: SummaryList = {\n classes: 'govuk-summary-list--long-actions',\n rows: []\n }\n\n let count = 0\n\n if (Array.isArray(list)) {\n count = list.length\n\n const summaryPath = this.getSummaryPath(request)\n\n list.forEach((item, index) => {\n const items: SummaryListAction[] = []\n\n // Remove summary list actions from previews\n if (!isForceAccess) {\n items.push({\n href: redirectPath(`${href}/${item.itemId}`, {\n returnUrl: query.returnUrl ?? this.getHref(summaryPath)\n }),\n text: 'Change',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: `item ${index + 1}`\n })\n\n if (count > 1) {\n items.push({\n href: redirectPath(`${href}/${item.itemId}/confirm-delete`, {\n returnUrl: query.returnUrl\n }),\n text: 'Remove',\n classes: 'govuk-link--no-visited-state',\n visuallyHiddenText: `item ${index + 1}`\n })\n }\n }\n\n const itemDisplayText = collection.fields.length\n ? collection.fields[0].getDisplayStringFromState(item)\n : ''\n\n summaryList.rows.push({\n key: {\n text: `${title} ${index + 1}`\n },\n value: {\n text: itemDisplayText || 'Not provided'\n },\n actions: {\n items\n }\n })\n })\n }\n\n const unit = count === 1 ? 'answer' : 'answers'\n\n return {\n ...this.viewModel,\n backLink: this.getBackLink(request, context),\n repeatTitle: title,\n pageTitle: `You have added ${count} ${unit}`,\n showTitle: true,\n context,\n errors,\n checkAnswers: [{ summaryList }],\n allowSaveAndExit: this.shouldShowSaveAndExit(request.server)\n }\n }\n\n getSummaryPath(request?: FormContextRequest) {\n const { path } = this\n\n const summaryPath = super.getSummaryPath()\n\n if (!request) {\n return summaryPath\n }\n\n const { query } = request\n\n return redirectPath(`${path}${summaryPath}`, {\n returnUrl: query.returnUrl\n })\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -26,9 +26,10 @@ export class PaymentService {
|
|
|
26
26
|
/**
|
|
27
27
|
* Captures a payment that is in 'capturable' status
|
|
28
28
|
* @param {string} paymentId
|
|
29
|
+
* @param {number} amount
|
|
29
30
|
* @returns {Promise<boolean>}
|
|
30
31
|
*/
|
|
31
|
-
capturePayment(paymentId: string): Promise<boolean>;
|
|
32
|
+
capturePayment(paymentId: string, amount: number): Promise<boolean>;
|
|
32
33
|
/**
|
|
33
34
|
* @param {CreatePaymentRequest} payload
|
|
34
35
|
*/
|
|
@@ -42,7 +42,14 @@ export class PaymentService {
|
|
|
42
42
|
return_url: returnUrl,
|
|
43
43
|
delayed_capture: true
|
|
44
44
|
});
|
|
45
|
-
logger.info(
|
|
45
|
+
logger.info({
|
|
46
|
+
event: {
|
|
47
|
+
category: 'payment',
|
|
48
|
+
action: 'create-payment',
|
|
49
|
+
outcome: 'success',
|
|
50
|
+
reference: response.payment_id
|
|
51
|
+
}
|
|
52
|
+
}, `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`);
|
|
46
53
|
return {
|
|
47
54
|
paymentId: response.payment_id,
|
|
48
55
|
paymentUrl: response._links.next_url.href
|
|
@@ -65,7 +72,15 @@ export class PaymentService {
|
|
|
65
72
|
throw new Error(`Failed to get payment status: ${errorMessage}`);
|
|
66
73
|
}
|
|
67
74
|
const state = response.payload.state;
|
|
68
|
-
logger.info(
|
|
75
|
+
logger.info({
|
|
76
|
+
event: {
|
|
77
|
+
category: 'payment',
|
|
78
|
+
action: 'get-payment-status',
|
|
79
|
+
outcome: state.status,
|
|
80
|
+
reason: `${state.code ?? 'N/A'} ${state.message ?? 'N/A'}`,
|
|
81
|
+
reference: paymentId
|
|
82
|
+
}
|
|
83
|
+
}, `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`);
|
|
69
84
|
return {
|
|
70
85
|
state,
|
|
71
86
|
_links: response.payload._links,
|
|
@@ -83,16 +98,24 @@ export class PaymentService {
|
|
|
83
98
|
/**
|
|
84
99
|
* Captures a payment that is in 'capturable' status
|
|
85
100
|
* @param {string} paymentId
|
|
101
|
+
* @param {number} amount
|
|
86
102
|
* @returns {Promise<boolean>}
|
|
87
103
|
*/
|
|
88
|
-
async capturePayment(paymentId) {
|
|
104
|
+
async capturePayment(paymentId, amount) {
|
|
89
105
|
try {
|
|
90
106
|
const response = await post(`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`, {
|
|
91
107
|
headers: getAuthHeaders(this.#apiKey)
|
|
92
108
|
});
|
|
93
109
|
const statusCode = response.res.statusCode;
|
|
94
110
|
if (statusCode === StatusCodes.OK || statusCode === StatusCodes.NO_CONTENT) {
|
|
95
|
-
logger.info(
|
|
111
|
+
logger.info({
|
|
112
|
+
event: {
|
|
113
|
+
category: 'payment',
|
|
114
|
+
action: 'capture-payment',
|
|
115
|
+
outcome: `success amount=${amount}`,
|
|
116
|
+
reference: paymentId
|
|
117
|
+
}
|
|
118
|
+
}, `[payment] Successfully captured payment for paymentId=${paymentId}`);
|
|
96
119
|
return true;
|
|
97
120
|
}
|
|
98
121
|
logger.error(`[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":["StatusCodes","createLogger","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","metadata","response","postToPayProvider","return_url","delayed_capture","info","payment_id","paymentId","paymentUrl","_links","next_url","href","getPaymentStatus","getByType","headers","json","error","errorMessage","Error","message","JSON","stringify","state","payload","status","code","email","err","capturePayment","statusCode","res","OK","NO_CONTENT","postJsonByType"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {{ formId: string, slug: string }} metadata\n */\n async createPayment(amount, description, returnUrl, reference, metadata) {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n }\n\n /**\n * @param {string} paymentId\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n `[payment] Got payment status for paymentId=${paymentId}: ${state.status} message:${state.message ?? 'N/A'} code:${state.code ?? 'N/A'}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"service.js","names":["StatusCodes","createLogger","get","post","postJson","PAYMENT_BASE_URL","PAYMENT_ENDPOINT","logger","getAuthHeaders","apiKey","Authorization","PaymentService","constructor","createPayment","amount","description","returnUrl","reference","metadata","response","postToPayProvider","return_url","delayed_capture","info","event","category","action","outcome","payment_id","paymentId","paymentUrl","_links","next_url","href","getPaymentStatus","getByType","headers","json","error","errorMessage","Error","message","JSON","stringify","state","payload","status","reason","code","email","err","capturePayment","statusCode","res","OK","NO_CONTENT","postJsonByType"],"sources":["../../../../src/server/plugins/payment/service.js"],"sourcesContent":["import { StatusCodes } from 'http-status-codes'\n\nimport { createLogger } from '~/src/server/common/helpers/logging/logger.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\nconst PAYMENT_BASE_URL = 'https://publicapi.payments.service.gov.uk'\nconst PAYMENT_ENDPOINT = '/v1/payments'\n\nconst logger = createLogger()\n\n/**\n * @param {string} apiKey\n * @returns {{ Authorization: string }}\n */\nfunction getAuthHeaders(apiKey) {\n return {\n Authorization: `Bearer ${apiKey}`\n }\n}\n\nexport class PaymentService {\n /** @type {string} */\n #apiKey\n\n /**\n * @param {string} apiKey - API key to use (global config for test value, per-form config for live value)\n */\n constructor(apiKey) {\n this.#apiKey = apiKey\n }\n\n /**\n * Creates a payment with delayed capture (pre-authorisation)\n * @param {number} amount - in pence\n * @param {string} description\n * @param {string} returnUrl\n * @param {string} reference\n * @param {{ formId: string, slug: string }} metadata\n */\n async createPayment(amount, description, returnUrl, reference, metadata) {\n const response = await this.postToPayProvider({\n amount,\n description,\n reference,\n metadata,\n return_url: returnUrl,\n delayed_capture: true\n })\n\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'create-payment',\n outcome: 'success',\n reference: response.payment_id\n }\n },\n `[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`\n )\n\n return {\n paymentId: response.payment_id,\n paymentUrl: response._links.next_url.href\n }\n }\n\n /**\n * @param {string} paymentId\n * @returns {Promise<GetPaymentResponse>}\n */\n async getPaymentStatus(paymentId) {\n const getByType = /** @type {typeof get<GetPaymentApiResponse>} */ (get)\n\n try {\n const response = await getByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}`,\n {\n headers: getAuthHeaders(this.#apiKey),\n json: true\n }\n )\n\n if (response.error) {\n const errorMessage =\n response.error instanceof Error\n ? response.error.message\n : JSON.stringify(response.error)\n throw new Error(`Failed to get payment status: ${errorMessage}`)\n }\n\n const state = response.payload.state\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'get-payment-status',\n outcome: state.status,\n reason: `${state.code ?? 'N/A'} ${state.message ?? 'N/A'}`,\n reference: paymentId\n }\n },\n `[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`\n )\n\n return {\n state,\n _links: response.payload._links,\n email: response.payload.email,\n paymentId: response.payload.payment_id,\n amount: response.payload.amount\n }\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error getting payment status for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * Captures a payment that is in 'capturable' status\n * @param {string} paymentId\n * @param {number} amount\n * @returns {Promise<boolean>}\n */\n async capturePayment(paymentId, amount) {\n try {\n const response = await post(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,\n {\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n const statusCode = response.res.statusCode\n\n if (\n statusCode === StatusCodes.OK ||\n statusCode === StatusCodes.NO_CONTENT\n ) {\n logger.info(\n {\n event: {\n category: 'payment',\n action: 'capture-payment',\n outcome: `success amount=${amount}`,\n reference: paymentId\n }\n },\n `[payment] Successfully captured payment for paymentId=${paymentId}`\n )\n return true\n }\n\n logger.error(\n `[payment] Capture failed for paymentId=${paymentId}: HTTP ${statusCode}`\n )\n return false\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error capturing payment for paymentId=${paymentId}: ${error.message}`\n )\n throw err\n }\n }\n\n /**\n * @param {CreatePaymentRequest} payload\n */\n async postToPayProvider(payload) {\n const postJsonByType =\n /** @type {typeof postJson<CreatePaymentResponse>} */ (postJson)\n\n try {\n const response = await postJsonByType(\n `${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}`,\n {\n payload,\n headers: getAuthHeaders(this.#apiKey)\n }\n )\n\n if (response.payload?.state.status !== 'created') {\n throw new Error(\n `Failed to create payment for reference=${payload.reference}`\n )\n }\n\n return response.payload\n } catch (err) {\n const error = /** @type {Error} */ (err)\n logger.error(\n error,\n `[payment] Error creating payment for reference=${payload.reference}: ${error.message}`\n )\n throw err\n }\n }\n}\n\n/**\n * @import { CreatePaymentRequest, CreatePaymentResponse, GetPaymentApiResponse, GetPaymentResponse } from '~/src/server/plugins/payment/types.js'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -120,7 +120,7 @@ describe('payment service', () => {
|
|
|
120
120
|
payload: capturePaymentResult,
|
|
121
121
|
error: undefined
|
|
122
122
|
});
|
|
123
|
-
const captureResult = await service.capturePayment('payment-id-12345');
|
|
123
|
+
const captureResult = await service.capturePayment('payment-id-12345', 100);
|
|
124
124
|
expect(captureResult).toBe(true);
|
|
125
125
|
});
|
|
126
126
|
it('should return true when successful capture with statusCode 204', async () => {
|
|
@@ -133,7 +133,7 @@ describe('payment service', () => {
|
|
|
133
133
|
payload: capturePaymentResult,
|
|
134
134
|
error: undefined
|
|
135
135
|
});
|
|
136
|
-
const captureResult = await service.capturePayment('payment-id-12345');
|
|
136
|
+
const captureResult = await service.capturePayment('payment-id-12345', 100);
|
|
137
137
|
expect(captureResult).toBe(true);
|
|
138
138
|
});
|
|
139
139
|
it('should return false when status code not 200 or 204', async () => {
|
|
@@ -146,12 +146,12 @@ describe('payment service', () => {
|
|
|
146
146
|
payload: capturePaymentResult,
|
|
147
147
|
error: undefined
|
|
148
148
|
});
|
|
149
|
-
const captureResult = await service.capturePayment('payment-id-12345');
|
|
149
|
+
const captureResult = await service.capturePayment('payment-id-12345', 100);
|
|
150
150
|
expect(captureResult).toBe(false);
|
|
151
151
|
});
|
|
152
152
|
it('should throw when internal error', async () => {
|
|
153
153
|
jest.mocked(post).mockRejectedValueOnce(new Error('internal capture error'));
|
|
154
|
-
await expect(() => service.capturePayment('payment-id-12345')).rejects.toThrow('internal capture error');
|
|
154
|
+
await expect(() => service.capturePayment('payment-id-12345', 100)).rejects.toThrow('internal capture error');
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.test.js","names":["PaymentService","get","post","postJson","jest","mock","describe","service","it","expect","toBeDefined","createPaymentResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","undefined","referenceNumber","returnUrl","metadata","formId","slug","payment","createPayment","paymentId","toBe","paymentUrl","mockRejectedValueOnce","Error","rejects","toThrow","getPaymentStatusResult","paymentStatus","getPaymentStatus","capturePaymentResult","captureResult","capturePayment"],"sources":["../../../../src/server/plugins/payment/service.test.js"],"sourcesContent":["import { PaymentService } from '~/src/server/plugins/payment/service.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment service', () => {\n const service = new PaymentService('my-api-key')\n describe('constructor', () => {\n it('should create instance', () => {\n expect(service).toBeDefined()\n })\n })\n\n describe('createPayment', () => {\n it('should create a payment', async () => {\n const createPaymentResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n const payment = await service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n expect(payment.paymentId).toBe('payment-id-12345')\n expect(payment.paymentUrl).toBe('http://next-url-href/payment')\n })\n\n it('should throw if fails to create a payment - failed API call', async () => {\n jest\n .mocked(postJson)\n .mockRejectedValueOnce(new Error('internal creation error'))\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('internal creation error')\n })\n\n it('should throw if fails to create a payment - bad result from API call', async () => {\n const createPaymentResult = {\n state: {\n status: 'failed'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('Failed to create payment')\n })\n })\n\n describe('getPaymentStatus', () => {\n it('should get payment status if exists', async () => {\n const getPaymentStatusResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusResult,\n error: undefined\n })\n\n const paymentStatus = await service.getPaymentStatus('payment-id-12345')\n expect(paymentStatus.paymentId).toBe('payment-id-12345')\n expect(paymentStatus._links.next_url?.href).toBe(\n 'http://next-url-href/payment'\n )\n })\n\n it('should handle payment status error', async () => {\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: undefined,\n error: new Error('some-error')\n })\n\n await expect(() =>\n service.getPaymentStatus('payment-id-12345')\n ).rejects.toThrow('Failed to get payment status: some-error')\n })\n })\n\n describe('capturePayment', () => {\n it('should return true when successful capture with statusCode 200', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment('payment-id-12345')\n expect(captureResult).toBe(true)\n })\n\n it('should return true when successful capture with statusCode 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 204,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment('payment-id-12345')\n expect(captureResult).toBe(true)\n })\n\n it('should return false when status code not 200 or 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 500,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment('payment-id-12345')\n expect(captureResult).toBe(false)\n })\n\n it('should throw when internal error', async () => {\n jest\n .mocked(post)\n .mockRejectedValueOnce(new Error('internal capture error'))\n\n await expect(() =>\n service.capturePayment('payment-id-12345')\n ).rejects.toThrow('internal capture error')\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"service.test.js","names":["PaymentService","get","post","postJson","jest","mock","describe","service","it","expect","toBeDefined","createPaymentResult","payment_id","_links","next_url","href","state","status","mocked","mockResolvedValueOnce","res","statusCode","headers","payload","error","undefined","referenceNumber","returnUrl","metadata","formId","slug","payment","createPayment","paymentId","toBe","paymentUrl","mockRejectedValueOnce","Error","rejects","toThrow","getPaymentStatusResult","paymentStatus","getPaymentStatus","capturePaymentResult","captureResult","capturePayment"],"sources":["../../../../src/server/plugins/payment/service.test.js"],"sourcesContent":["import { PaymentService } from '~/src/server/plugins/payment/service.js'\nimport { get, post, postJson } from '~/src/server/services/httpService.js'\n\njest.mock('~/src/server/services/httpService.ts')\n\ndescribe('payment service', () => {\n const service = new PaymentService('my-api-key')\n describe('constructor', () => {\n it('should create instance', () => {\n expect(service).toBeDefined()\n })\n })\n\n describe('createPayment', () => {\n it('should create a payment', async () => {\n const createPaymentResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n const payment = await service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n expect(payment.paymentId).toBe('payment-id-12345')\n expect(payment.paymentUrl).toBe('http://next-url-href/payment')\n })\n\n it('should throw if fails to create a payment - failed API call', async () => {\n jest\n .mocked(postJson)\n .mockRejectedValueOnce(new Error('internal creation error'))\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('internal creation error')\n })\n\n it('should throw if fails to create a payment - bad result from API call', async () => {\n const createPaymentResult = {\n state: {\n status: 'failed'\n }\n }\n jest.mocked(postJson).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: createPaymentResult,\n error: undefined\n })\n\n const referenceNumber = 'ABC-DEF-123'\n const returnUrl = 'http://localhost:3009/payment-callback-handler'\n const metadata = { formId: 'form-id', slug: 'my-form-slug' }\n await expect(() =>\n service.createPayment(\n 100,\n 'Payment description',\n returnUrl,\n referenceNumber,\n metadata\n )\n ).rejects.toThrow('Failed to create payment')\n })\n })\n\n describe('getPaymentStatus', () => {\n it('should get payment status if exists', async () => {\n const getPaymentStatusResult = {\n payment_id: 'payment-id-12345',\n _links: {\n next_url: {\n href: 'http://next-url-href/payment'\n }\n },\n state: {\n status: 'created'\n }\n }\n\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: getPaymentStatusResult,\n error: undefined\n })\n\n const paymentStatus = await service.getPaymentStatus('payment-id-12345')\n expect(paymentStatus.paymentId).toBe('payment-id-12345')\n expect(paymentStatus._links.next_url?.href).toBe(\n 'http://next-url-href/payment'\n )\n })\n\n it('should handle payment status error', async () => {\n jest.mocked(get).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: undefined,\n error: new Error('some-error')\n })\n\n await expect(() =>\n service.getPaymentStatus('payment-id-12345')\n ).rejects.toThrow('Failed to get payment status: some-error')\n })\n })\n\n describe('capturePayment', () => {\n it('should return true when successful capture with statusCode 200', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 200,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return true when successful capture with statusCode 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 204,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(true)\n })\n\n it('should return false when status code not 200 or 204', async () => {\n const capturePaymentResult = {}\n jest.mocked(post).mockResolvedValueOnce({\n res: /** @type {IncomingMessage} */ ({\n statusCode: 500,\n headers: {}\n }),\n payload: capturePaymentResult,\n error: undefined\n })\n\n const captureResult = await service.capturePayment(\n 'payment-id-12345',\n 100\n )\n expect(captureResult).toBe(false)\n })\n\n it('should throw when internal error', async () => {\n jest\n .mocked(post)\n .mockRejectedValueOnce(new Error('internal capture error'))\n\n await expect(() =>\n service.capturePayment('payment-id-12345', 100)\n ).rejects.toThrow('internal capture error')\n })\n })\n})\n\n/**\n * @import { IncomingMessage } from 'node:http'\n */\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -297,7 +297,10 @@ export class PaymentField extends FormComponent {
|
|
|
297
297
|
)
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
const captured = await paymentService.capturePayment(
|
|
300
|
+
const captured = await paymentService.capturePayment(
|
|
301
|
+
paymentId,
|
|
302
|
+
status.amount
|
|
303
|
+
)
|
|
301
304
|
|
|
302
305
|
if (!captured) {
|
|
303
306
|
throw new PaymentPreAuthError(
|
|
@@ -67,9 +67,9 @@ describe('SummaryViewModel', () => {
|
|
|
67
67
|
} satisfies FormState,
|
|
68
68
|
keys: [
|
|
69
69
|
'How would you like to receive your pizza?',
|
|
70
|
-
'
|
|
70
|
+
'Pizza',
|
|
71
71
|
'How you would like to receive your pizza',
|
|
72
|
-
'
|
|
72
|
+
'Pizza',
|
|
73
73
|
'Pizza'
|
|
74
74
|
],
|
|
75
75
|
values: ['Collection', 'Not provided'],
|
|
@@ -91,13 +91,13 @@ describe('SummaryViewModel', () => {
|
|
|
91
91
|
} satisfies FormState,
|
|
92
92
|
keys: [
|
|
93
93
|
'How would you like to receive your pizza?',
|
|
94
|
-
'Pizza
|
|
94
|
+
'Pizza',
|
|
95
95
|
'How you would like to receive your pizza',
|
|
96
|
-
'
|
|
96
|
+
'Pizza',
|
|
97
97
|
'Pizza'
|
|
98
98
|
],
|
|
99
|
-
values: ['Delivery', 'You added 1
|
|
100
|
-
answers: ['Delivery', 'You added 1
|
|
99
|
+
values: ['Delivery', 'You have added 1 answer'],
|
|
100
|
+
answers: ['Delivery', 'You have added 1 answer'],
|
|
101
101
|
names: ['orderType', 'pizza']
|
|
102
102
|
},
|
|
103
103
|
{
|
|
@@ -120,13 +120,13 @@ describe('SummaryViewModel', () => {
|
|
|
120
120
|
} satisfies FormState,
|
|
121
121
|
keys: [
|
|
122
122
|
'How would you like to receive your pizza?',
|
|
123
|
-
'
|
|
123
|
+
'Pizza',
|
|
124
124
|
'How you would like to receive your pizza',
|
|
125
|
-
'
|
|
125
|
+
'Pizza',
|
|
126
126
|
'Pizza'
|
|
127
127
|
],
|
|
128
|
-
values: ['Delivery', 'You added 2
|
|
129
|
-
answers: ['Delivery', 'You added 2
|
|
128
|
+
values: ['Delivery', 'You have added 2 answers'],
|
|
129
|
+
answers: ['Delivery', 'You have added 2 answers'],
|
|
130
130
|
names: ['orderType', 'pizza']
|
|
131
131
|
}
|
|
132
132
|
])(
|
|
@@ -326,7 +326,7 @@ describe('SummaryViewModel', () => {
|
|
|
326
326
|
expect(details1.items[0]).toMatchObject({
|
|
327
327
|
name: 'pizza',
|
|
328
328
|
value: '',
|
|
329
|
-
title: '
|
|
329
|
+
title: 'Pizza',
|
|
330
330
|
label: 'Pizza'
|
|
331
331
|
})
|
|
332
332
|
|
|
@@ -186,13 +186,13 @@ function ItemRepeat(
|
|
|
186
186
|
const { name, title } = repeat.options
|
|
187
187
|
|
|
188
188
|
const values = page.getListFromState(state)
|
|
189
|
-
const unit = values.length === 1 ?
|
|
189
|
+
const unit = values.length === 1 ? 'answer' : 'answers'
|
|
190
190
|
|
|
191
191
|
return {
|
|
192
192
|
name,
|
|
193
193
|
label: title,
|
|
194
|
-
title
|
|
195
|
-
value: values.length ? `You added ${values.length} ${unit}` : '',
|
|
194
|
+
title,
|
|
195
|
+
value: values.length ? `You have added ${values.length} ${unit}` : '',
|
|
196
196
|
href: getPageHref(page, options.path, {
|
|
197
197
|
returnUrl: getPageHref(page, page.getSummaryPath())
|
|
198
198
|
}),
|
|
@@ -5,7 +5,7 @@ exports[`SummaryViewModel Check answers (0 items) should use correct summary lab
|
|
|
5
5
|
{
|
|
6
6
|
"label": "Pizza",
|
|
7
7
|
"name": "pizza",
|
|
8
|
-
"title": "
|
|
8
|
+
"title": "Pizza",
|
|
9
9
|
"value": "",
|
|
10
10
|
},
|
|
11
11
|
{
|
|
@@ -22,8 +22,8 @@ exports[`SummaryViewModel Check answers (1 item) should use correct summary labe
|
|
|
22
22
|
{
|
|
23
23
|
"label": "Pizza",
|
|
24
24
|
"name": "pizza",
|
|
25
|
-
"title": "Pizza
|
|
26
|
-
"value": "You added 1
|
|
25
|
+
"title": "Pizza",
|
|
26
|
+
"value": "You have added 1 answer",
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"label": "How would you like to receive your pizza?",
|
|
@@ -39,8 +39,8 @@ exports[`SummaryViewModel Check answers (2 items) should use correct summary lab
|
|
|
39
39
|
{
|
|
40
40
|
"label": "Pizza",
|
|
41
41
|
"name": "pizza",
|
|
42
|
-
"title": "
|
|
43
|
-
"value": "You added 2
|
|
42
|
+
"title": "Pizza",
|
|
43
|
+
"value": "You have added 2 answers",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"label": "How would you like to receive your pizza?",
|
|
@@ -149,7 +149,7 @@ describe('RepeatPageController', () => {
|
|
|
149
149
|
description: 'No items',
|
|
150
150
|
list: [] satisfies RepeatListState,
|
|
151
151
|
viewModel: {
|
|
152
|
-
pageTitle: 'You have added 0
|
|
152
|
+
pageTitle: 'You have added 0 answers',
|
|
153
153
|
showTitle: true,
|
|
154
154
|
sectionTitle: 'Food'
|
|
155
155
|
}
|
|
@@ -164,7 +164,7 @@ describe('RepeatPageController', () => {
|
|
|
164
164
|
}
|
|
165
165
|
] satisfies RepeatListState,
|
|
166
166
|
viewModel: {
|
|
167
|
-
pageTitle: 'You have added 1
|
|
167
|
+
pageTitle: 'You have added 1 answer',
|
|
168
168
|
showTitle: true,
|
|
169
169
|
sectionTitle: 'Food'
|
|
170
170
|
}
|
|
@@ -184,7 +184,7 @@ describe('RepeatPageController', () => {
|
|
|
184
184
|
}
|
|
185
185
|
] satisfies RepeatListState,
|
|
186
186
|
viewModel: {
|
|
187
|
-
pageTitle: 'You have added 2
|
|
187
|
+
pageTitle: 'You have added 2 answers',
|
|
188
188
|
showTitle: true,
|
|
189
189
|
sectionTitle: 'Food'
|
|
190
190
|
}
|
|
@@ -207,7 +207,7 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
207
207
|
) => {
|
|
208
208
|
const { path, repeat } = this
|
|
209
209
|
const { query } = request
|
|
210
|
-
const { schema
|
|
210
|
+
const { schema } = repeat
|
|
211
211
|
const { state } = context
|
|
212
212
|
|
|
213
213
|
const list = this.getListFromState(state)
|
|
@@ -232,7 +232,7 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
232
232
|
// Show error if repeat limits apply
|
|
233
233
|
if (hasErrorMin || hasErrorMax) {
|
|
234
234
|
const count = hasErrorMax ? schema.max : schema.min
|
|
235
|
-
const itemTitle =
|
|
235
|
+
const itemTitle = `answer${count === 1 ? '' : 's'}`
|
|
236
236
|
|
|
237
237
|
context.errors = [
|
|
238
238
|
{
|
|
@@ -295,9 +295,9 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
295
295
|
...viewModel,
|
|
296
296
|
context,
|
|
297
297
|
backLink: this.getBackLink(request, context),
|
|
298
|
-
pageTitle:
|
|
298
|
+
pageTitle: 'Are you sure you want to remove this answer?',
|
|
299
299
|
itemTitle: `${title} ${list.indexOf(item) + 1}`,
|
|
300
|
-
buttonConfirm: { text:
|
|
300
|
+
buttonConfirm: { text: 'Remove' },
|
|
301
301
|
buttonCancel: { text: 'Cancel' }
|
|
302
302
|
} satisfies ItemDeletePageViewModel)
|
|
303
303
|
}
|
|
@@ -431,11 +431,13 @@ export class RepeatPageController extends QuestionPageController {
|
|
|
431
431
|
})
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
const unit = count === 1 ? 'answer' : 'answers'
|
|
435
|
+
|
|
434
436
|
return {
|
|
435
437
|
...this.viewModel,
|
|
436
438
|
backLink: this.getBackLink(request, context),
|
|
437
439
|
repeatTitle: title,
|
|
438
|
-
pageTitle: `You have added ${count} ${
|
|
440
|
+
pageTitle: `You have added ${count} ${unit}`,
|
|
439
441
|
showTitle: true,
|
|
440
442
|
context,
|
|
441
443
|
errors,
|
|
@@ -48,6 +48,14 @@ export class PaymentService {
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
logger.info(
|
|
51
|
+
{
|
|
52
|
+
event: {
|
|
53
|
+
category: 'payment',
|
|
54
|
+
action: 'create-payment',
|
|
55
|
+
outcome: 'success',
|
|
56
|
+
reference: response.payment_id
|
|
57
|
+
}
|
|
58
|
+
},
|
|
51
59
|
`[payment] Created payment and user taken to enter pre-auth details for paymentId=${response.payment_id}`
|
|
52
60
|
)
|
|
53
61
|
|
|
@@ -83,7 +91,16 @@ export class PaymentService {
|
|
|
83
91
|
|
|
84
92
|
const state = response.payload.state
|
|
85
93
|
logger.info(
|
|
86
|
-
|
|
94
|
+
{
|
|
95
|
+
event: {
|
|
96
|
+
category: 'payment',
|
|
97
|
+
action: 'get-payment-status',
|
|
98
|
+
outcome: state.status,
|
|
99
|
+
reason: `${state.code ?? 'N/A'} ${state.message ?? 'N/A'}`,
|
|
100
|
+
reference: paymentId
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
`[payment] Got payment status for paymentId=${paymentId}: status=${state.status}`
|
|
87
104
|
)
|
|
88
105
|
|
|
89
106
|
return {
|
|
@@ -106,9 +123,10 @@ export class PaymentService {
|
|
|
106
123
|
/**
|
|
107
124
|
* Captures a payment that is in 'capturable' status
|
|
108
125
|
* @param {string} paymentId
|
|
126
|
+
* @param {number} amount
|
|
109
127
|
* @returns {Promise<boolean>}
|
|
110
128
|
*/
|
|
111
|
-
async capturePayment(paymentId) {
|
|
129
|
+
async capturePayment(paymentId, amount) {
|
|
112
130
|
try {
|
|
113
131
|
const response = await post(
|
|
114
132
|
`${PAYMENT_BASE_URL}${PAYMENT_ENDPOINT}/${paymentId}/capture`,
|
|
@@ -124,6 +142,14 @@ export class PaymentService {
|
|
|
124
142
|
statusCode === StatusCodes.NO_CONTENT
|
|
125
143
|
) {
|
|
126
144
|
logger.info(
|
|
145
|
+
{
|
|
146
|
+
event: {
|
|
147
|
+
category: 'payment',
|
|
148
|
+
action: 'capture-payment',
|
|
149
|
+
outcome: `success amount=${amount}`,
|
|
150
|
+
reference: paymentId
|
|
151
|
+
}
|
|
152
|
+
},
|
|
127
153
|
`[payment] Successfully captured payment for paymentId=${paymentId}`
|
|
128
154
|
)
|
|
129
155
|
return true
|
|
@@ -154,7 +154,10 @@ describe('payment service', () => {
|
|
|
154
154
|
error: undefined
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
const captureResult = await service.capturePayment(
|
|
157
|
+
const captureResult = await service.capturePayment(
|
|
158
|
+
'payment-id-12345',
|
|
159
|
+
100
|
|
160
|
+
)
|
|
158
161
|
expect(captureResult).toBe(true)
|
|
159
162
|
})
|
|
160
163
|
|
|
@@ -169,7 +172,10 @@ describe('payment service', () => {
|
|
|
169
172
|
error: undefined
|
|
170
173
|
})
|
|
171
174
|
|
|
172
|
-
const captureResult = await service.capturePayment(
|
|
175
|
+
const captureResult = await service.capturePayment(
|
|
176
|
+
'payment-id-12345',
|
|
177
|
+
100
|
|
178
|
+
)
|
|
173
179
|
expect(captureResult).toBe(true)
|
|
174
180
|
})
|
|
175
181
|
|
|
@@ -184,7 +190,10 @@ describe('payment service', () => {
|
|
|
184
190
|
error: undefined
|
|
185
191
|
})
|
|
186
192
|
|
|
187
|
-
const captureResult = await service.capturePayment(
|
|
193
|
+
const captureResult = await service.capturePayment(
|
|
194
|
+
'payment-id-12345',
|
|
195
|
+
100
|
|
196
|
+
)
|
|
188
197
|
expect(captureResult).toBe(false)
|
|
189
198
|
})
|
|
190
199
|
|
|
@@ -194,7 +203,7 @@ describe('payment service', () => {
|
|
|
194
203
|
.mockRejectedValueOnce(new Error('internal capture error'))
|
|
195
204
|
|
|
196
205
|
await expect(() =>
|
|
197
|
-
service.capturePayment('payment-id-12345')
|
|
206
|
+
service.capturePayment('payment-id-12345', 100)
|
|
198
207
|
).rejects.toThrow('internal capture error')
|
|
199
208
|
})
|
|
200
209
|
})
|