From 2a0bb56651129e02b0a740013dc4c2c9f4fa2c8b Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Mon, 22 Jun 2026 12:21:50 +0100 Subject: [PATCH 1/4] Includes all contact details --- .../models/unavailable-view-model.test.ts | 40 +------------------ .../engine/models/unavailable-view-model.ts | 20 ++++------ .../plugins/engine/views/unavailable.html | 30 ++++++++++---- 3 files changed, 30 insertions(+), 60 deletions(-) diff --git a/src/server/plugins/engine/models/unavailable-view-model.test.ts b/src/server/plugins/engine/models/unavailable-view-model.test.ts index dc9ab8636..fc9e00280 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.test.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.test.ts @@ -19,44 +19,6 @@ describe('unavailableViewModel', () => { ...metadata, organisation: 'Rural Payments Agency – RPA' } as FormMetadata) - expect(result.organisationName).toBe('Rural Payments Agency') - }) - - it('should handle multiple phone lines correctly', () => { - const result = unavailableViewModel({ - ...metadata, - contact: { - phone: '01234 567 890\n09876 543 210' - } - } as FormMetadata) - expect(result.phoneLines).toEqual(['01234 567 890', '09876 543 210']) - }) - - it('should filter out empty phone lines and trim whitespace', () => { - const result = unavailableViewModel({ - ...metadata, - contact: { - phone: ' 01234 567 890 \n \n 09876 543 210 ' - } - } as FormMetadata) - expect(result.phoneLines).toEqual(['01234 567 890', '09876 543 210']) - }) - - it('should return undefined if phone is empty or only whitespace', () => { - const result = unavailableViewModel({ - ...metadata, - contact: { - phone: ' \n ' - } - } as FormMetadata) - expect(result.phoneLines).toBeUndefined() - }) - - it('should handle missing contact property', () => { - const result = unavailableViewModel({ - ...metadata, - contact: undefined - } as FormMetadata) - expect(result.phoneLines).toBeUndefined() + expect(result.organisationName).toBe('the Rural Payments Agency') }) }) diff --git a/src/server/plugins/engine/models/unavailable-view-model.ts b/src/server/plugins/engine/models/unavailable-view-model.ts index 244607516..9fe3533cc 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.ts @@ -1,10 +1,10 @@ -import { type FormMetadata } from '@defra/forms-model' +import { type FormMetadata, type FormMetadataContact } from '@defra/forms-model' export interface UnavailableViewModel { pageTitle: string formTitle: string organisationName: string - phoneLines?: string[] + contact?: FormMetadataContact } /** @@ -12,16 +12,10 @@ export interface UnavailableViewModel { * "Rural Payments Agency – RPA". The unavailable page reads cleanly without it. */ function stripOrgSuffix(organisation: string) { - return organisation.split(' – ')[0] -} - -function splitPhoneLines(phone: string | undefined) { - if (!phone) return undefined - const lines = phone - .split('\n') - .map((line) => line.trim()) - .filter((line) => line.length > 0) - return lines.length > 0 ? lines : undefined + const orgName = organisation.split(' – ')[0] + return orgName === 'Defra' || orgName === 'Natural England' + ? orgName + : `the ${orgName}` } export function unavailableViewModel( @@ -31,6 +25,6 @@ export function unavailableViewModel( pageTitle: 'Sorry, this form is unavailable', formTitle: metadata.title, organisationName: stripOrgSuffix(metadata.organisation), - phoneLines: splitPhoneLines(metadata.contact?.phone) + contact: metadata.contact } } diff --git a/src/server/plugins/engine/views/unavailable.html b/src/server/plugins/engine/views/unavailable.html index f82fb10c9..02a8fbf42 100644 --- a/src/server/plugins/engine/views/unavailable.html +++ b/src/server/plugins/engine/views/unavailable.html @@ -4,17 +4,31 @@

Sorry, this form is unavailable

-

'{{ formTitle }}' has been archived and is no longer available.

-

Contact the {{ organisationName }}.

+

'{{ formTitle }}' is no longer available.

+

Contact details for {{ organisationName }}:

- {% if phoneLines %} -
    - {% for line in phoneLines %} -
  • {{ line }}
  • - {% endfor %} -
+ {% if contact.phone %} +

Telephone

+
+ {{ contact.phone | markdown(3) | safe }} +

Find out about call charges

{% endif %} + + {% if contact.email %} +

Email

+ + {% endif %} + + {% if contact.online %} +

Online contact form

+ + {% endif %}
{% endblock %} From 2954c99533935a1f0ce7dd8f8e08dc982ad5bc55 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Tue, 23 Jun 2026 09:13:16 +0100 Subject: [PATCH 2/4] Only shows 'Service unavailable' for live form (non-preview) --- .../plugins/engine/beta/form-context.ts | 4 +- .../plugins/engine/form-availability.test.ts | 40 +++++++++++++++++-- .../plugins/engine/form-availability.ts | 14 +++++-- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/server/plugins/engine/beta/form-context.ts b/src/server/plugins/engine/beta/form-context.ts index 88b06839d..c3c4f4786 100644 --- a/src/server/plugins/engine/beta/form-context.ts +++ b/src/server/plugins/engine/beta/form-context.ts @@ -53,7 +53,7 @@ export async function getFormModel( const formState = resolveState(state) const metadata = await formsService.getFormMetadata(slug) - assertFormAvailable(metadata) + assertFormAvailable(metadata, formState, isPreview) const definition = await formsService.getFormDefinition( metadata.id, @@ -136,9 +136,9 @@ export async function resolveFormModel( const { formsService } = services const metadata = await formsService.getFormMetadata(slug) - assertFormAvailable(metadata) const formState = resolveState(state) const isPreview = options.isPreview ?? isPreviewState(state, options) + assertFormAvailable(metadata, formState, isPreview) const stateMetadata = metadata[formState] if (!stateMetadata) { diff --git a/src/server/plugins/engine/form-availability.test.ts b/src/server/plugins/engine/form-availability.test.ts index aa50c8f95..341c76275 100644 --- a/src/server/plugins/engine/form-availability.test.ts +++ b/src/server/plugins/engine/form-availability.test.ts @@ -1,3 +1,4 @@ +import { FormStatus } from '@defra/forms-model' import Boom from '@hapi/boom' import { @@ -8,17 +9,48 @@ import { metadata } from '~/test/fixtures/form.js' describe('form-availability', () => { describe('assertFormAvailable', () => { - it('should do nothing if form is online', () => { + it('should do nothing if live form is online', () => { expect(() => - assertFormAvailable({ ...metadata, offline: false }) + assertFormAvailable( + { ...metadata, offline: false }, + FormStatus.Live, + false + ) ).not.toThrow() expect(() => - assertFormAvailable({ ...metadata, offline: undefined }) + assertFormAvailable( + { ...metadata, offline: undefined }, + FormStatus.Live, + false + ) + ).not.toThrow() + }) + + it('should do nothing if draft form or live preview is online', () => { + expect(() => + assertFormAvailable( + { ...metadata, offline: false }, + FormStatus.Draft, + true + ) + ).not.toThrow() + expect(() => + assertFormAvailable( + { ...metadata, offline: undefined }, + FormStatus.Live, + true + ) ).not.toThrow() }) it('should throw a 503 Boom error if form is offline', () => { - expect(() => assertFormAvailable({ ...metadata, offline: true })).toThrow( + expect(() => + assertFormAvailable( + { ...metadata, offline: true }, + FormStatus.Live, + false + ) + ).toThrow( expect.objectContaining({ message: `Form ${metadata.slug} is offline`, data: { diff --git a/src/server/plugins/engine/form-availability.ts b/src/server/plugins/engine/form-availability.ts index 214df81fb..de140534e 100644 --- a/src/server/plugins/engine/form-availability.ts +++ b/src/server/plugins/engine/form-availability.ts @@ -1,4 +1,4 @@ -import { type FormMetadata } from '@defra/forms-model' +import { FormStatus, type FormMetadata } from '@defra/forms-model' import Boom from '@hapi/boom' export interface OfflineBoomData { @@ -11,8 +11,16 @@ export interface OfflineBoomData { * unavailable-response extension catches the marker and renders the * "Sorry, this form is unavailable" view at HTTP 200. */ -export function assertFormAvailable(metadata: FormMetadata): void { - if (metadata.offline === true) { +export function assertFormAvailable( + metadata: FormMetadata, + formState: FormStatus, + isPreview: boolean +): void { + if ( + metadata.offline === true && + formState === FormStatus.Live && + !isPreview + ) { const data: OfflineBoomData = { offline: true, metadata } throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), { statusCode: 503, From 49e1c698e8c0a0f53669d4f94f5cd097533e2db9 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Tue, 23 Jun 2026 12:48:21 +0100 Subject: [PATCH 3/4] Moved files out of 'beta' folder --- .../engine/{beta => }/form-context.test.ts | 17 +++++++++-------- .../plugins/engine/{beta => }/form-context.ts | 0 src/server/plugins/engine/index.ts | 2 +- src/server/plugins/engine/routes/index.ts | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) rename src/server/plugins/engine/{beta => }/form-context.test.ts (95%) rename src/server/plugins/engine/{beta => }/form-context.ts (100%) diff --git a/src/server/plugins/engine/beta/form-context.test.ts b/src/server/plugins/engine/form-context.test.ts similarity index 95% rename from src/server/plugins/engine/beta/form-context.test.ts rename to src/server/plugins/engine/form-context.test.ts index aed997266..c6f4aa340 100644 --- a/src/server/plugins/engine/beta/form-context.test.ts +++ b/src/server/plugins/engine/form-context.test.ts @@ -6,7 +6,7 @@ import { getFormModel, resolveFormModel, type FormModelOptions -} from '~/src/server/plugins/engine/beta/form-context.js' +} from '~/src/server/plugins/engine/form-context.js' import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js' import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js' import { type FormContext } from '~/src/server/plugins/engine/types.js' @@ -17,7 +17,7 @@ const mockGetCacheService = jest.fn() const mockCacheService = { getState: jest.fn() } const mockCheckEmailAddressForLiveFormSubmission = jest.fn() -jest.mock('../models/index.ts', () => ({ +jest.mock('~/src/server/plugins/engine/models/index.ts', () => ({ __esModule: true, FormModel: jest.fn() })) @@ -32,7 +32,7 @@ jest.mock('~/src/server/plugins/engine/services/index.js', () => ({ outputService: {} })) -jest.mock('../pageControllers/index.ts', () => { +jest.mock('~/src/server/plugins/engine/pageControllers/index.ts', () => { class MockTerminalPageController { path = '' } @@ -43,8 +43,8 @@ jest.mock('../pageControllers/index.ts', () => { } }) -jest.mock('../helpers.ts', () => ({ - ...jest.requireActual('../helpers.ts'), +jest.mock('~/src/server/plugins/engine/helpers.ts', () => ({ + ...jest.requireActual('~/src/server/plugins/engine/helpers.ts'), getCacheService: (...args: unknown[]): unknown => mockGetCacheService(...args), checkEmailAddressForLiveFormSubmission: (...args: unknown[]): unknown => @@ -56,13 +56,14 @@ const mockServices: { } = jest.requireMock('~/src/server/plugins/engine/services/index.js') const mockFormsService = mockServices.formsService -const { FormModel }: { FormModel: jest.Mock } = - jest.requireMock('../models/index.ts') +const { FormModel }: { FormModel: jest.Mock } = jest.requireMock( + '~/src/server/plugins/engine/models/index.ts' +) const { TerminalPageController: MockTerminalPageController }: { TerminalPageController: new () => { path: string } } = jest.requireMock( - '../pageControllers/index.ts' + '~/src/server/plugins/engine/pageControllers/index.ts' ) describe('getFormContext helper', () => { diff --git a/src/server/plugins/engine/beta/form-context.ts b/src/server/plugins/engine/form-context.ts similarity index 100% rename from src/server/plugins/engine/beta/form-context.ts rename to src/server/plugins/engine/form-context.ts diff --git a/src/server/plugins/engine/index.ts b/src/server/plugins/engine/index.ts index 1e62f5e67..fa0efdfaa 100644 --- a/src/server/plugins/engine/index.ts +++ b/src/server/plugins/engine/index.ts @@ -19,7 +19,7 @@ export { getFormContext, getFormModel, resolveFormModel -} from '~/src/server/plugins/engine/beta/form-context.js' +} from '~/src/server/plugins/engine/form-context.js' export * from '~/src/server/plugins/engine/form-availability.js' const globals = { diff --git a/src/server/plugins/engine/routes/index.ts b/src/server/plugins/engine/routes/index.ts index ef324cd6b..de05dcb5b 100644 --- a/src/server/plugins/engine/routes/index.ts +++ b/src/server/plugins/engine/routes/index.ts @@ -10,11 +10,11 @@ import { EXTERNAL_STATE_APPENDAGE, EXTERNAL_STATE_PAYLOAD } from '~/src/server/constants.js' -import { resolveFormModel } from '~/src/server/plugins/engine/beta/form-context.js' import { FormComponent, isFormState } from '~/src/server/plugins/engine/components/FormComponent.js' +import { resolveFormModel } from '~/src/server/plugins/engine/form-context.js' import { checkFormStatus, findPage, From aa05b3c2e5dcf0b8d0081b21dd47b97a0ddce090 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Tue, 23 Jun 2026 14:22:22 +0100 Subject: [PATCH 4/4] Removed stripOrgName and related --- .../engine/models/unavailable-view-model.test.ts | 4 ++-- .../plugins/engine/models/unavailable-view-model.ts | 13 +------------ src/server/plugins/engine/views/unavailable.html | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/server/plugins/engine/models/unavailable-view-model.test.ts b/src/server/plugins/engine/models/unavailable-view-model.test.ts index fc9e00280..bc8f2ad80 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.test.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.test.ts @@ -17,8 +17,8 @@ describe('unavailableViewModel', () => { it('should strip the organisation suffix if present', () => { const result = unavailableViewModel({ ...metadata, - organisation: 'Rural Payments Agency – RPA' + organisation: 'Rural Payments Agency - RPA' } as FormMetadata) - expect(result.organisationName).toBe('the Rural Payments Agency') + expect(result.organisationName).toBe('Rural Payments Agency - RPA') }) }) diff --git a/src/server/plugins/engine/models/unavailable-view-model.ts b/src/server/plugins/engine/models/unavailable-view-model.ts index 9fe3533cc..f69d477b9 100644 --- a/src/server/plugins/engine/models/unavailable-view-model.ts +++ b/src/server/plugins/engine/models/unavailable-view-model.ts @@ -7,24 +7,13 @@ export interface UnavailableViewModel { contact?: FormMetadataContact } -/** - * Defra organisations carry an abbreviation suffix on the enum value, e.g. - * "Rural Payments Agency – RPA". The unavailable page reads cleanly without it. - */ -function stripOrgSuffix(organisation: string) { - const orgName = organisation.split(' – ')[0] - return orgName === 'Defra' || orgName === 'Natural England' - ? orgName - : `the ${orgName}` -} - export function unavailableViewModel( metadata: FormMetadata ): UnavailableViewModel { return { pageTitle: 'Sorry, this form is unavailable', formTitle: metadata.title, - organisationName: stripOrgSuffix(metadata.organisation), + organisationName: metadata.organisation, contact: metadata.contact } } diff --git a/src/server/plugins/engine/views/unavailable.html b/src/server/plugins/engine/views/unavailable.html index 02a8fbf42..6d61a5ff8 100644 --- a/src/server/plugins/engine/views/unavailable.html +++ b/src/server/plugins/engine/views/unavailable.html @@ -5,7 +5,7 @@

Sorry, this form is unavailable

'{{ formTitle }}' is no longer available.

-

Contact details for {{ organisationName }}:

+

Contact details for {{ organisationName }}

{% if contact.phone %}

Telephone