Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/server/plugins/engine/models/FormModel.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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')

Expand Down Expand Up @@ -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'
)
})
})
})
20 changes: 20 additions & 0 deletions src/server/plugins/engine/models/FormModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
formDefinitionV2Schema,
generateConditionAlias,
hasComponents,
hasComponentsEvenIfNoNext,
hasRepeater,
isConditionWrapperV2,
yesNoListId,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ describe('v1 human formatter', () => {

const itemsPayment = getFormSubmissionData(
summaryViewModelPayment.context,
summaryViewModelPayment.details
summaryViewModelPayment.details,
modelPayment
)

it('should add payment details', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/server/plugins/engine/outputFormatters/human/v1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ describe('v1 human formatter', () => {

const items = getFormSubmissionData(
summaryViewModel.context,
summaryViewModel.details
summaryViewModel.details,
model
)

describe('getPersonalisation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ const summaryViewModel = controller.getSummaryViewModel(request, context)

const items = getFormSubmissionData(
summaryViewModel.context,
summaryViewModel.details
summaryViewModel.details,
model
)

describe('getPersonalisation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ describe('getPersonalisation', () => {

const items = getFormSubmissionData(
summaryViewModel.context,
summaryViewModel.details
summaryViewModel.details,
model
)

const body = format(context, items, model, submitResponse, formStatus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -387,7 +390,14 @@ describe('SummaryPageController - Payment (DF-832)', () => {
contact: { online: { url: '/help' } }
} as unknown as Parameters<typeof submitForm>[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 () => {
Expand Down Expand Up @@ -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: ''
})
})
})
})
19 changes: 13 additions & 6 deletions src/server/plugins/engine/pageControllers/SummaryPageController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ export async function submitForm(

const items = getFormSubmissionData(
summaryViewModel.context,
summaryViewModel.details
summaryViewModel.details,
model
)

try {
Expand Down Expand Up @@ -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 }) =>
Expand All @@ -544,7 +548,7 @@ export function getFormSubmissionData(context: FormContext, details: Detail[]) {
)
.flat()

const paymentItems = getPaymentFieldItems(context)
const paymentItems = getPaymentFieldItems(context, model)

return [...items, ...paymentItems]
}
Expand All @@ -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({
Expand Down
6 changes: 5 additions & 1 deletion src/server/plugins/engine/routes/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading