diff --git a/src/server/plugins/engine/models/FormModel.test.ts b/src/server/plugins/engine/models/FormModel.test.ts index 4ae416e60..284fad39a 100644 --- a/src/server/plugins/engine/models/FormModel.test.ts +++ b/src/server/plugins/engine/models/FormModel.test.ts @@ -1,8 +1,11 @@ import { + ComponentType, SchemaVersion, formDefinitionSchema, formDefinitionV2Schema, - type FormDefinition + type ComponentDef, + type FormDefinition, + type PageQuestion } from '@defra/forms-model' import { todayAsDateOnly } from '~/src/server/plugins/engine/date-helper.js' @@ -16,6 +19,7 @@ import conditionsListDefinition from '~/test/form/definitions/conditions-list.js import relativeDatesDefinition from '~/test/form/definitions/conditions-relative-dates-v2.js' import fieldsRequiredDefinition from '~/test/form/definitions/fields-required.js' import joinedConditionsDefinition from '~/test/form/definitions/joined-conditions-simple-v2.js' +import paymentDefinition from '~/test/form/definitions/payment.js' jest.mock('~/src/server/plugins/engine/date-helper.ts') @@ -722,4 +726,36 @@ describe('FormModel - Joined Conditions', () => { expect(model.getSection('nonexistent')).toBeUndefined() }) }) + + describe('moreThanOnePaymentQuestion', () => { + it('should return false if no payment questions', () => { + const model = new FormModel(definition, { basePath: 'test' }) + expect(model.moreThanOnePaymentQuestion()).toBe(false) + }) + + it('should return false if only one payment question', () => { + const definition = { + ...paymentDefinition + } + + const model = new FormModel(definition, { basePath: 'test' }) + expect(model.moreThanOnePaymentQuestion()).toBe(false) + }) + + it('should throw if more than one payment questions', () => { + const definition = { + ...paymentDefinition + } + const extraPaymentComponent = { + type: ComponentType.PaymentField, + name: 'paymentField' + } as ComponentDef + const page = definition.pages[0] as PageQuestion + page.components.push(extraPaymentComponent) + + expect(() => new FormModel(definition, { basePath: 'test' })).toThrow( + 'Invalid form definition: Only one payment question is allowed per form' + ) + }) + }) }) diff --git a/src/server/plugins/engine/models/FormModel.ts b/src/server/plugins/engine/models/FormModel.ts index 30f6c2463..5520597ce 100644 --- a/src/server/plugins/engine/models/FormModel.ts +++ b/src/server/plugins/engine/models/FormModel.ts @@ -9,6 +9,7 @@ import { formDefinitionV2Schema, generateConditionAlias, hasComponents, + hasComponentsEvenIfNoNext, hasRepeater, isConditionWrapperV2, yesNoListId, @@ -159,6 +160,13 @@ export class FormModel { this.services = services this.controllers = controllers + // Assert that there is only one payment question (if any) + if (this.moreThanOnePaymentQuestion()) { + throw new Error( + 'Invalid form definition: Only one payment question is allowed per form' + ) + } + this.pageDefMap = new Map(def.pages.map((page) => [page.path, page])) this.listDefMap = new Map(def.lists.map((list) => [list.name, list])) this.listDefIdMap = new Map( @@ -543,6 +551,18 @@ export class FormModel { .filter(isConditionWrapperV2) .find((condition) => condition.id === conditionId) } + + /** + * Checks that only one payment field exists (if any payments fields exist) + */ + moreThanOnePaymentQuestion() { + const numOfPaymentFields = this.def.pages + .flatMap((page) => + hasComponentsEvenIfNoNext(page) ? page.components : [] + ) + .filter((comp) => comp.type === ComponentType.PaymentField).length + return numOfPaymentFields > 1 + } } /** diff --git a/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts b/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts index d4f8c5129..23b3f54ef 100644 --- a/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts +++ b/src/server/plugins/engine/outputFormatters/human/v1.payment.test.ts @@ -82,7 +82,8 @@ describe('v1 human formatter', () => { const itemsPayment = getFormSubmissionData( summaryViewModelPayment.context, - summaryViewModelPayment.details + summaryViewModelPayment.details, + modelPayment ) it('should add payment details', () => { diff --git a/src/server/plugins/engine/outputFormatters/human/v1.test.ts b/src/server/plugins/engine/outputFormatters/human/v1.test.ts index d4ebb36d8..4089bb5d7 100644 --- a/src/server/plugins/engine/outputFormatters/human/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/human/v1.test.ts @@ -70,7 +70,8 @@ describe('v1 human formatter', () => { const items = getFormSubmissionData( summaryViewModel.context, - summaryViewModel.details + summaryViewModel.details, + model ) describe('getPersonalisation', () => { diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts b/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts index c16a81288..35448bf2d 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.payment.test.ts @@ -79,7 +79,8 @@ const summaryViewModel = controller.getSummaryViewModel(request, context) const items = getFormSubmissionData( summaryViewModel.context, - summaryViewModel.details + summaryViewModel.details, + model ) describe('getPersonalisation', () => { diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.test.ts b/src/server/plugins/engine/outputFormatters/machine/v2.test.ts index 78703fc3d..9ac6d8229 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.test.ts @@ -280,7 +280,8 @@ describe('getPersonalisation', () => { const items = getFormSubmissionData( summaryViewModel.context, - summaryViewModel.details + summaryViewModel.details, + model ) const body = format(context, items, model, submitResponse, formStatus) diff --git a/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts b/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts index 4c704d111..86677bb59 100644 --- a/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts +++ b/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts @@ -356,12 +356,15 @@ describe('SummaryPageController - Payment (DF-832)', () => { } as FormSubmissionState) const outputSubmit = jest.fn() + const formSubmissionSubmit = jest.fn() model.services = { ...model.services, formSubmissionService: { ...model.services.formSubmissionService, - submit: jest.fn().mockResolvedValue({ data: { reference: 'r' } }) + submit: formSubmissionSubmit.mockResolvedValue({ + data: { reference: 'r' } + }) }, outputService: { ...model.services.outputService, @@ -387,7 +390,14 @@ describe('SummaryPageController - Payment (DF-832)', () => { contact: { online: { url: '/help' } } } as unknown as Parameters[1] - return { request, context, viewModel, formMetadata, outputSubmit } + return { + request, + context, + viewModel, + formMetadata, + outputSubmit, + formSubmissionSubmit + } } it('re-throws as PaymentSubmissionError when outputService fails and a payment has been captured', async () => { @@ -428,5 +438,53 @@ describe('SummaryPageController - Payment (DF-832)', () => { ) ).rejects.toBe(err) }) + + it('submits with correct payload', async () => { + const { + request, + context, + viewModel, + formMetadata, + formSubmissionSubmit + } = buildSubmitHarness({ captured: true }) + + await submitForm( + context, + formMetadata, + request, + viewModel, + model, + 'notify@example.com' + ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const paymentCall = formSubmissionSubmit.mock.calls[0][0] + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const paymentItems = paymentCall.main as unknown as { + name: string + title: string + value: string + }[] + expect(paymentItems).toHaveLength(4) + expect(paymentItems[0]).toEqual({ + name: 'paymentField_paymentDescription', + title: 'Payment description', + value: 'Test payment' + }) + expect(paymentItems[1]).toEqual({ + name: 'paymentField_paymentAmount', + title: 'Payment amount', + value: '£99.00' + }) + expect(paymentItems[2]).toEqual({ + name: 'paymentField_paymentReference', + title: 'Payment reference', + value: 'ref-1' + }) + expect(paymentItems[3]).toEqual({ + name: 'paymentField_paymentDate', + title: 'Payment date', + value: '' + }) + }) }) }) diff --git a/src/server/plugins/engine/pageControllers/SummaryPageController.ts b/src/server/plugins/engine/pageControllers/SummaryPageController.ts index 2863e51ba..20178d700 100644 --- a/src/server/plugins/engine/pageControllers/SummaryPageController.ts +++ b/src/server/plugins/engine/pageControllers/SummaryPageController.ts @@ -433,7 +433,8 @@ export async function submitForm( const items = getFormSubmissionData( summaryViewModel.context, - summaryViewModel.details + summaryViewModel.details, + model ) try { @@ -531,11 +532,14 @@ function submitData( main: buildMainRecords(items), repeaters: buildRepeaterRecords(items) } - return submit(payload) } -export function getFormSubmissionData(context: FormContext, details: Detail[]) { +export function getFormSubmissionData( + context: FormContext, + details: Detail[], + model: FormModel +) { const items = context.relevantPages .map(({ href }) => details.flatMap(({ items }) => @@ -544,7 +548,7 @@ export function getFormSubmissionData(context: FormContext, details: Detail[]) { ) .flat() - const paymentItems = getPaymentFieldItems(context) + const paymentItems = getPaymentFieldItems(context, model) return [...items, ...paymentItems] } @@ -553,10 +557,13 @@ export function getFormSubmissionData(context: FormContext, details: Detail[]) { * Gets DetailItems for PaymentField components * PaymentField is excluded from summaryDetails for UI but needs to be in submission data */ -function getPaymentFieldItems(context: FormContext): DetailItemField[] { +function getPaymentFieldItems( + context: FormContext, + model: FormModel +): DetailItemField[] { const items: DetailItemField[] = [] - for (const page of context.relevantPages) { + for (const page of model.pages) { for (const field of page.collection.fields) { if (field instanceof PaymentField) { items.push({ diff --git a/src/server/plugins/engine/routes/questions.ts b/src/server/plugins/engine/routes/questions.ts index e69c3e2d6..374e471d9 100644 --- a/src/server/plugins/engine/routes/questions.ts +++ b/src/server/plugins/engine/routes/questions.ts @@ -59,7 +59,11 @@ async function handleHttpEvent( // TODO: Update structured data POST payload with when helper // is updated to removing the dependency on `SummaryViewModel` etc. const viewModel = new SummaryViewModel(request, page, context) - const items = getFormSubmissionData(viewModel.context, viewModel.details) + const items = getFormSubmissionData( + viewModel.context, + viewModel.details, + model + ) // @ts-expect-error - function signature will be refactored in the next iteration of the formatter const payload = format(context, items, model, undefined, undefined)