diff --git a/package-lock.json b/package-lock.json index 5b6bffd00..d9e29b1e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.506", + "@defra/forms-model": "^3.0.552", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", @@ -2272,9 +2272,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.506", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.506.tgz", - "integrity": "sha512-Ac2ES6RdhZ44mmO9MQ40iZUVOdM0P8dqc2FfTQ8IqbFrbiTdUd5jcCAtn40HaU4tEg3v7qKFHlJC4XSUKm/eaA==", + "version": "3.0.552", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.552.tgz", + "integrity": "sha512-g2ZpUHL+CQyJbGX1kBpM7pXy33GVKMaS/95it0tJXkkNECO+0pkBdvu2EVrUD6GMHAirvWzhvFVA0y1CX8rP3g==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", diff --git a/package.json b/package.json index c7ffb9a56..caa3b3131 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.506", + "@defra/forms-model": "^3.0.552", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", diff --git a/src/server/plugins/engine/models/FormModel.test.ts b/src/server/plugins/engine/models/FormModel.test.ts index 8154c3c77..cd77cfcec 100644 --- a/src/server/plugins/engine/models/FormModel.test.ts +++ b/src/server/plugins/engine/models/FormModel.test.ts @@ -141,6 +141,21 @@ describe('FormModel', () => { expect(model.schemaVersion).toBe(SchemaVersion.V1) }) + it('sets versionNumber from options', () => { + const model = new FormModel(definition, { + basePath: 'test', + versionNumber: 42 + }) + + expect(model.versionNumber).toBe(42) + }) + + it('sets versionNumber to undefined when not provided', () => { + const model = new FormModel(definition, { basePath: 'test' }) + + expect(model.versionNumber).toBeUndefined() + }) + it.each([ { input: undefined, @@ -329,6 +344,55 @@ describe('FormModel', () => { ) }) + it('includes submittedVersionNumber in context when versionNumber is set', () => { + const formModel = new FormModel(fieldsRequiredDefinition, { + basePath: '/components', + versionNumber: 123 + }) + + const state = { + $$__referenceNumber: 'foobar' + } + const pageUrl = new URL('http://example.com/components/fields-required') + + const request: FormContextRequest = buildFormContextRequest({ + method: 'get', + query: {}, + path: pageUrl.pathname, + params: { path: 'components', slug: 'fields-required' }, + url: pageUrl, + app: { model: formModel } + }) + + const context = formModel.getFormContext(request, state) + + expect(context.submittedVersionNumber).toBe(123) + }) + + it('sets submittedVersionNumber to undefined when versionNumber is not set', () => { + const formModel = new FormModel(fieldsRequiredDefinition, { + basePath: '/components' + }) + + const state = { + $$__referenceNumber: 'foobar' + } + const pageUrl = new URL('http://example.com/components/fields-required') + + const request: FormContextRequest = buildFormContextRequest({ + method: 'get', + query: {}, + path: pageUrl.pathname, + params: { path: 'components', slug: 'fields-required' }, + url: pageUrl, + app: { model: formModel } + }) + + const context = formModel.getFormContext(request, state) + + expect(context.submittedVersionNumber).toBeUndefined() + }) + it('redirects to the page if the list field (radio) is invalidated due to list item conditions', () => { const formModel = new FormModel(conditionsListDefinition, { basePath: '/conditional-list-items' diff --git a/src/server/plugins/engine/models/FormModel.ts b/src/server/plugins/engine/models/FormModel.ts index 21c838d91..4c9277400 100644 --- a/src/server/plugins/engine/models/FormModel.ts +++ b/src/server/plugins/engine/models/FormModel.ts @@ -76,6 +76,7 @@ export class FormModel { name: string values: FormDefinition basePath: string + versionNumber?: number conditions: Partial> pages: PageControllerClass[] services: Services @@ -94,7 +95,7 @@ export class FormModel { constructor( def: typeof this.def, - options: { basePath: string }, + options: { basePath: string; versionNumber?: number }, services: Services = defaultServices, controllers?: Record ) { @@ -148,6 +149,7 @@ export class FormModel { this.name = def.name ?? '' this.values = result.value this.basePath = options.basePath + this.versionNumber = options.versionNumber this.conditions = {} this.services = services this.controllers = controllers @@ -344,7 +346,8 @@ export class FormModel { componentDefMap: this.componentDefMap, pageMap: this.pageMap, componentMap: this.componentMap, - referenceNumber: getReferenceNumber(state) + referenceNumber: getReferenceNumber(state), + submittedVersionNumber: this.versionNumber } // Validate current page diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts index e4657c42d..7c3f115cd 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts @@ -11,7 +11,10 @@ import { type DetailItemField, type DetailItemRepeat } from '~/src/server/plugins/engine/models/types.js' -import { format } from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js' +import { + format, + getVersionMetadata +} from '~/src/server/plugins/engine/outputFormatters/adapter/v1.js' import { buildFormContextRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js' import { FormAdapterSubmissionSchemaVersion } from '~/src/server/plugins/engine/types/index.js' import { @@ -207,7 +210,7 @@ describe('Adapter v1 formatter', () => { }) it('should return the adapter v1 output with complete formMetadata', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { id: 'form-123', slug: 'test-form', title: 'Test Form', @@ -225,7 +228,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -284,7 +287,7 @@ describe('Adapter v1 formatter', () => { }) it('should handle preview form status correctly', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { id: 'form-123', slug: 'test-form', title: 'Test Form', @@ -302,7 +305,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -327,7 +330,7 @@ describe('Adapter v1 formatter', () => { }) it('should handle partial formMetadata', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { id: 'form-456', slug: 'partial-form', title: 'Partial Form' @@ -344,7 +347,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -452,7 +455,7 @@ describe('Adapter v1 formatter', () => { }) it('should handle formMetadata with only id', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { id: 'only-id-form' } as FormMetadata @@ -467,7 +470,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -477,7 +480,7 @@ describe('Adapter v1 formatter', () => { }) it('should handle formMetadata with only slug', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { slug: 'only-slug-form' } as FormMetadata @@ -492,7 +495,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -502,7 +505,7 @@ describe('Adapter v1 formatter', () => { }) it('should handle formMetadata with only notificationEmail', () => { - const formMetadata: FormMetadata = { + const formMetadata: Partial = { notificationEmail: 'only-email@example.com' } as FormMetadata @@ -517,7 +520,7 @@ describe('Adapter v1 formatter', () => { model, submitResponse, formStatus, - formMetadata + formMetadata as FormMetadata ) const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload @@ -693,4 +696,434 @@ describe('Adapter v1 formatter', () => { } }) }) + + it('should handle missing versionMetadata gracefully', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com' + } as FormMetadata + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toBeUndefined() + }) + + describe('version metadata handling', () => { + it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 2, + createdAt: new Date('2024-01-15T00:00:00.000Z') + } + ] + } + + const modelWithVersion = new FormModel(definition, { + basePath: 'test', + versionNumber: 2 + }) + + const contextWithVersion = modelWithVersion.getFormContext(request, state) + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + contextWithVersion, + items, + modelWithVersion, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toEqual({ + versionNumber: 2, + createdAt: '2024-01-15T00:00:00.000Z' + }) + }) + + it('should use first version as fallback when submittedVersionNumber is undefined', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 2, + createdAt: new Date('2024-01-15T00:00:00.000Z') + } + ] + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toEqual({ + versionNumber: 1, + createdAt: '2024-01-01T00:00:00.000Z' + }) + }) + + it('should not include versionMetadata when submittedVersionNumber is undefined and no versions exist', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com' + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toBeUndefined() + }) + + it('should not include versionMetadata when submittedVersionNumber is undefined and versions array is empty', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [] + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toBeUndefined() + }) + + it('should not include versionMetadata when submittedVersionNumber does not match any version', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 2, + createdAt: new Date('2024-01-15T00:00:00.000Z') + } + ] + } + + const modelWithVersion = new FormModel(definition, { + basePath: 'test', + versionNumber: 99 // Non-existent version + }) + + const contextWithVersion = modelWithVersion.getFormContext(request, state) + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + contextWithVersion, + items, + modelWithVersion, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + // Should fall back to first version since submittedVersionNumber doesn't match + expect(parsedBody.meta.versionMetadata).toEqual({ + versionNumber: 1, + createdAt: '2024-01-01T00:00:00.000Z' + }) + }) + + it('should use first version as fallback when submittedVersionNumber does not match any version', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 2, + createdAt: new Date('2024-01-15T00:00:00.000Z') + } + ] + } + + const modelWithVersion = new FormModel(definition, { + basePath: 'test', + versionNumber: 99 // Non-existent version + }) + + const contextWithVersion = modelWithVersion.getFormContext(request, state) + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + contextWithVersion, + items, + modelWithVersion, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + // Should fall back to first version since submittedVersionNumber doesn't match + expect(parsedBody.meta.versionMetadata).toEqual({ + versionNumber: 1, + createdAt: '2024-01-01T00:00:00.000Z' + }) + }) + + it('should handle single version in versions array', () => { + const formMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 5, + createdAt: new Date('2024-02-01T00:00:00.000Z') + } + ] + } + + const formStatus = { + isPreview: false, + state: FormStatus.Live + } + + const body = format( + context, + items, + model, + submitResponse, + formStatus, + formMetadata as FormMetadata + ) + const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload + + expect(parsedBody.meta.versionMetadata).toEqual({ + versionNumber: 5, + createdAt: '2024-02-01T00:00:00.000Z' + }) + }) + }) + + describe('getVersionMetadata', () => { + const mockFormMetadata: Partial = { + id: 'form-123', + slug: 'test-form', + title: 'Test Form', + notificationEmail: 'test@example.com', + versions: [ + { + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 2, + createdAt: new Date('2024-01-02T00:00:00.000Z') + }, + { + versionNumber: 3, + createdAt: new Date('2024-01-03T00:00:00.000Z') + } + ] + } + + it('should return undefined when no form metadata provided', () => { + const result = getVersionMetadata(1, undefined) + expect(result).toBeUndefined() + }) + + it('should return undefined when form metadata has no versions', () => { + const formMetadataWithoutVersions: Partial = { + ...mockFormMetadata, + versions: undefined + } + + const result = getVersionMetadata( + 1, + formMetadataWithoutVersions as FormMetadata + ) + expect(result).toBeUndefined() + }) + + it('should return undefined when versions array is empty', () => { + const formMetadataWithEmptyVersions: Partial = { + ...mockFormMetadata, + versions: [] + } + + const result = getVersionMetadata( + 1, + formMetadataWithEmptyVersions as FormMetadata + ) + expect(result).toBeUndefined() + }) + + it('should return specific version when submittedVersionNumber matches', () => { + const result = getVersionMetadata(2, mockFormMetadata as FormMetadata) + expect(result).toEqual({ + versionNumber: 2, + createdAt: new Date('2024-01-02T00:00:00.000Z') + }) + }) + + it('should return first version when submittedVersionNumber not found', () => { + const result = getVersionMetadata(999, mockFormMetadata as FormMetadata) + expect(result).toEqual({ + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }) + }) + + it('should return first version when no submittedVersionNumber provided', () => { + const result = getVersionMetadata( + undefined, + mockFormMetadata as FormMetadata + ) + expect(result).toEqual({ + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }) + }) + + it('should handle single version in versions array', () => { + const singleVersionMetadata: Partial = { + ...mockFormMetadata, + versions: [ + { + versionNumber: 5, + createdAt: new Date('2024-02-01T00:00:00.000Z') + } + ] + } + + const result = getVersionMetadata( + undefined, + singleVersionMetadata as FormMetadata + ) + expect(result).toEqual({ + versionNumber: 5, + createdAt: new Date('2024-02-01T00:00:00.000Z') + }) + }) + + it('should return correct version when submittedVersionNumber is 0', () => { + const metadataWithVersionZero: Partial = { + ...mockFormMetadata, + versions: [ + { + versionNumber: 0, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }, + { + versionNumber: 1, + createdAt: new Date('2024-01-02T00:00:00.000Z') + } + ] + } + + const result = getVersionMetadata( + 0, + metadataWithVersionZero as FormMetadata + ) + expect(result).toEqual({ + versionNumber: 0, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }) + }) + + it('should handle negative submittedVersionNumber by falling back to first version', () => { + const result = getVersionMetadata(-1, mockFormMetadata as FormMetadata) + expect(result).toEqual({ + versionNumber: 1, + createdAt: new Date('2024-01-01T00:00:00.000Z') + }) + }) + }) }) diff --git a/src/server/plugins/engine/outputFormatters/adapter/v1.ts b/src/server/plugins/engine/outputFormatters/adapter/v1.ts index a2fc5ab47..4f528f88f 100644 --- a/src/server/plugins/engine/outputFormatters/adapter/v1.ts +++ b/src/server/plugins/engine/outputFormatters/adapter/v1.ts @@ -40,6 +40,11 @@ export function format( const transformedData = v2DataParsed.data + const versionMetadata = getVersionMetadata( + context.submittedVersionNumber, + formMetadata + ) + const meta: FormAdapterSubmissionMessageMeta = { schemaVersion: FormAdapterSubmissionSchemaVersion.V1, timestamp: new Date(), @@ -51,6 +56,10 @@ export function format( isPreview: formStatus.isPreview, notificationEmail: formMetadata?.notificationEmail ?? '' } + + if (versionMetadata) { + meta.versionMetadata = versionMetadata + } const data: FormAdapterSubmissionMessageData = transformedData const result: FormAdapterSubmissionMessageResult = { @@ -66,6 +75,34 @@ export function format( return JSON.stringify(payload) } +export function getVersionMetadata( + submittedVersionNumber: number | undefined, + formMetadata?: FormMetadata +): { versionNumber: number; createdAt: Date } | undefined { + if (!formMetadata?.versions?.length) { + return undefined + } + + if (submittedVersionNumber !== undefined) { + const submittedVersion = formMetadata.versions.find( + (v) => v.versionNumber === submittedVersionNumber + ) + if (submittedVersion) { + return { + versionNumber: submittedVersion.versionNumber, + createdAt: submittedVersion.createdAt + } + } + } + + // fallback to first available version + const firstVersion = formMetadata.versions[0] + return { + versionNumber: firstVersion.versionNumber, + createdAt: firstVersion.createdAt + } +} + function extractCsvFiles( submitResponse: SubmitResponsePayload ): FormAdapterSubmissionMessageResult['files'] { diff --git a/src/server/plugins/engine/outputFormatters/machine/v2.ts b/src/server/plugins/engine/outputFormatters/machine/v2.ts index 215fb33d0..a62a51ed3 100644 --- a/src/server/plugins/engine/outputFormatters/machine/v2.ts +++ b/src/server/plugins/engine/outputFormatters/machine/v2.ts @@ -29,13 +29,15 @@ export function format( const categorisedData = categoriseData(items) + const meta: Record = { + schemaVersion: '2', + timestamp: now.toISOString(), + definition: model.def, + referenceNumber: context.referenceNumber + } + const data = { - meta: { - schemaVersion: '2', - timestamp: now.toISOString(), - definition: model.def, - referenceNumber: context.referenceNumber - }, + meta, data: categorisedData } diff --git a/src/server/plugins/engine/routes/index.ts b/src/server/plugins/engine/routes/index.ts index c34b2ad53..93ea2584c 100644 --- a/src/server/plugins/engine/routes/index.ts +++ b/src/server/plugins/engine/routes/index.ts @@ -151,10 +151,12 @@ export function makeLoadFormPreHandler(server: Server, options: PluginOptions) { : `${prefix}/${slug}` ).substring(1) + const versionNumber = metadata.versions?.[0]?.versionNumber + // Construct the form model const model = new FormModel( definition, - { basePath }, + { basePath, versionNumber }, services, controllers ) diff --git a/src/server/plugins/engine/types.ts b/src/server/plugins/engine/types.ts index 7576545d7..a505252f6 100644 --- a/src/server/plugins/engine/types.ts +++ b/src/server/plugins/engine/types.ts @@ -3,6 +3,7 @@ import { type Event, type FormDefinition, type FormMetadata, + type FormVersionMetadata, type Item, type List, type Page @@ -179,6 +180,7 @@ export interface FormContext { pageMap: Map componentMap: Map referenceNumber: string + submittedVersionNumber?: number } export type FormContextRequest = ( @@ -405,6 +407,7 @@ export interface FormAdapterSubmissionMessageMeta { status: FormStatus isPreview: boolean notificationEmail: string + versionMetadata?: FormVersionMetadata } export type FormAdapterSubmissionMessageMetaSerialised = Omit< diff --git a/src/server/plugins/engine/types/schema.test.ts b/src/server/plugins/engine/types/schema.test.ts index 4bbd324b1..b07a48392 100644 --- a/src/server/plugins/engine/types/schema.test.ts +++ b/src/server/plugins/engine/types/schema.test.ts @@ -156,5 +156,45 @@ describe('Schema validation', () => { formAdapterSubmissionMessagePayloadSchema.validate(payloadWithoutData) expect(error).toBeDefined() }) + + it('should validate payload with versionMetadata', () => { + const payloadWithVersion = { + ...validPayload, + meta: { + ...validPayload.meta, + versionMetadata: { + versionNumber: 19, + createdAt: new Date('2025-09-08T09:28:15.576Z') + } + } + } + const { error } = + formAdapterSubmissionMessagePayloadSchema.validate(payloadWithVersion) + expect(error).toBeUndefined() + }) + + it('should validate payload without versionMetadata', () => { + const { error } = + formAdapterSubmissionMessagePayloadSchema.validate(validPayload) + expect(error).toBeUndefined() + }) + + it('should reject invalid versionMetadata', () => { + const payloadWithInvalidVersion = { + ...validPayload, + meta: { + ...validPayload.meta, + versionMetadata: { + versionNumber: 'not-a-number', // Invalid - should be number + createdAt: new Date('2025-09-08T09:28:15.576Z') + } + } + } + const { error } = formAdapterSubmissionMessagePayloadSchema.validate( + payloadWithInvalidVersion + ) + expect(error).toBeDefined() + expect(error?.message).toContain('must be a number') + }) }) }) diff --git a/src/server/plugins/engine/types/schema.ts b/src/server/plugins/engine/types/schema.ts index 57331bd1e..396d965cb 100644 --- a/src/server/plugins/engine/types/schema.ts +++ b/src/server/plugins/engine/types/schema.ts @@ -1,5 +1,6 @@ import { FormStatus, + formVersionMetadataSchema, idSchema, notificationEmailAddressSchema, slugSchema, @@ -29,7 +30,8 @@ export const formAdapterSubmissionMessageMetaSchema = .valid(...Object.values(FormStatus)) .required(), isPreview: Joi.boolean().required(), - notificationEmail: notificationEmailAddressSchema.required() + notificationEmail: notificationEmailAddressSchema.required(), + versionMetadata: formVersionMetadataSchema.optional() }) export const formAdapterSubmissionMessageDataSchema =