From 95a89ecd06d558eeb377ef14681e1da95cfdc5ee Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 8 Oct 2025 08:34:33 +0100 Subject: [PATCH 01/12] Sends user confirmation email --- src/lib/manager.js | 21 +++++- src/service/index.js | 4 +- src/service/mappers/user-confirmation.js | 45 ++++++++++++ src/service/notify.js | 87 ++++++++++++++++++++++-- src/service/notify.test.js | 16 ++--- 5 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 src/service/mappers/user-confirmation.js diff --git a/src/lib/manager.js b/src/lib/manager.js index d286781..0731a15 100644 --- a/src/lib/manager.js +++ b/src/lib/manager.js @@ -33,5 +33,24 @@ export async function getFormDefinition(formId, formStatus, versionNumber) { } /** - * @import { FormDefinition } from '@defra/forms-model' + * Gets the form metadata from the Forms Manager API + * @param {string} formId + * @returns {Promise} + */ +export async function getFormMetadata(formId) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!managerUrl) { + // TS / eslint conflict + throw new Error('Missing MANAGER_URL') + } + + const formUrl = new URL(`/forms/${formId}`, managerUrl) + + const { body } = await getJson(formUrl) + + return body +} + +/** + * @import { FormDefinition, FormMetadata } from '@defra/forms-model' */ diff --git a/src/service/index.js b/src/service/index.js index c69e1df..65878c7 100644 --- a/src/service/index.js +++ b/src/service/index.js @@ -1,5 +1,5 @@ import { handleFormSubmissionEvents } from '~/src/service/events.js' -import { sendNotifyEmail } from '~/src/service/notify.js' +import { sendNotifyEmails } from '~/src/service/notify.js' /** * @param {Message[]} messages @@ -7,7 +7,7 @@ import { sendNotifyEmail } from '~/src/service/notify.js' */ export async function handleEvent(messages) { const service = { - handleFormSubmission: sendNotifyEmail + handleFormSubmission: sendNotifyEmails } return handleFormSubmissionEvents(messages, service) } diff --git a/src/service/mappers/user-confirmation.js b/src/service/mappers/user-confirmation.js new file mode 100644 index 0000000..37d2603 --- /dev/null +++ b/src/service/mappers/user-confirmation.js @@ -0,0 +1,45 @@ +import { escapeMarkdown } from '@defra/forms-engine-plugin/engine/components/helpers/index.js' + +import { format as dateFormat } from '~/src/helpers/date.js' + +/** + * @param {string} formName + * @param {Date} submissionDate + * @param {FormMetadata} metadata + */ +export function getUserConfirmationEmailBody( + formName, + submissionDate, + metadata +) { + const formattedSubmissionDate = `${dateFormat(submissionDate, 'h:mmaaa')} on ${dateFormat(submissionDate, 'eeee d MMMM yyyy')}` + + const { submissionGuidance, organisation, contact } = metadata + + const phoneDetails = contact?.phone ? `${contact.phone}\n\n` : '' + const emailDetails = contact?.email + ? `${contact.email.address} ${contact.email.responseTime}\n\n` + : '' + const onlineDetails = contact?.online + ? `${contact.online.url} ${contact.online.text}\n\n` + : '' + const contactDetails = `${phoneDetails}${emailDetails}${onlineDetails}` + + return ` +# We have your form +We received your form submission for “${formName}’ on ${formattedSubmissionDate}. + +## What happens next +${submissionGuidance} + +## Get help +${contactDetails} +Do not reply to this emall. We do not monitor reples to this email address. + +From ${escapeMarkdown(organisation)} +` +} + +/** + * @import { FormMetadata } from '@defra/forms-model' + */ diff --git a/src/service/notify.js b/src/service/notify.js index c58d1d8..00eeda1 100644 --- a/src/service/notify.js +++ b/src/service/notify.js @@ -1,4 +1,5 @@ import { escapeMarkdown } from '@defra/forms-engine-plugin/engine/components/helpers/index.js' +import { getFormMetadata } from '@defra/forms-engine-plugin/services/formsService.js' import { getErrorMessage } from '@defra/forms-model' import { config } from '~/src/config/index.js' @@ -6,6 +7,7 @@ import { createLogger } from '~/src/helpers/logging/logger.js' import { getFormDefinition } from '~/src/lib/manager.js' import { sendNotification } from '~/src/lib/notify.js' import { getFormatter } from '~/src/service/mappers/formatters/index.js' +import { getUserConfirmationEmailBody } from '~/src/service/mappers/user-confirmation.js' // @ts-expect-error - incorrect typings in convict const templateId = /** @type {string} */ (config.get('notifyTemplateId')) @@ -17,7 +19,18 @@ const logger = createLogger() * @param {FormAdapterSubmissionMessage} formSubmissionMessage * @returns {Promise} */ -export async function sendNotifyEmail(formSubmissionMessage) { +export async function sendNotifyEmails(formSubmissionMessage) { + // TODO - decide how to handle if first email throws + await sendInternalEmail(formSubmissionMessage) + await sendUserConfirmationEmail(formSubmissionMessage) +} + +/** + * Sends an internal email to notify (to the form's submission inbox) + * @param {FormAdapterSubmissionMessage} formSubmissionMessage + * @returns {Promise} + */ +export async function sendInternalEmail(formSubmissionMessage) { const { formName: formNameInput, formId, @@ -29,10 +42,13 @@ export async function sendNotifyEmail(formSubmissionMessage) { const logTags = ['submit', 'email'] // Get submission email personalisation - logger.info(logTags, 'Getting personalisation data') + logger.info( + logTags, + 'Getting personalisation data - internal submission email' + ) logger.debug( - `Getting form definition: ${formId} version: ${versionMetadata?.versionNumber}` + `Getting form definition: ${formId} version: ${versionMetadata?.versionNumber} - internal submission email` ) const definition = await getFormDefinition( @@ -58,7 +74,7 @@ export async function sendNotifyEmail(formSubmissionMessage) { body = Buffer.from(body).toString('base64') } - logger.info(logTags, 'Sending email') + logger.info(logTags, 'Sending internal submission email') try { // Send submission email @@ -71,17 +87,76 @@ export async function sendNotifyEmail(formSubmissionMessage) { } }) - logger.info(logTags, 'Email sent successfully') + logger.info(logTags, 'Internal submission email sent successfully') } catch (err) { const errMsg = getErrorMessage(err) logger.error( err, - `[emailSendFailed] Error sending notification email - templateId: ${templateId} - ${errMsg}` + `[emailSendFailed] Error sending internal submission email - templateId: ${templateId} - ${errMsg}` ) throw err } } + +/** + * Sends a confirmation email to the submitting user + * @param {FormAdapterSubmissionMessage} formSubmissionMessage + * @returns {Promise} + */ +export async function sendUserConfirmationEmail(formSubmissionMessage) { + if (!formSubmissionMessage.meta.userConfirmationEmail) { + return + } + + const { + formId, + formName: formNameInput, + isPreview + } = formSubmissionMessage.meta + const logTags = ['submit', 'email'] + + // Get submission email personalisation + logger.info(logTags, 'Getting personalisation data - user confirmation email') + + const formName = escapeMarkdown(formNameInput) + const emailAddress = formSubmissionMessage.meta.userConfirmationEmail + + const metadata = await getFormMetadata(formId) + + const subject = isPreview + ? `TEST FORM CONFIRMATION: ${metadata.organisation}` + : `Form submitted to ${metadata.organisation}` + + logger.info(logTags, 'Sending user confirmation email') + + if (!metadata.submissionGuidance) { + throw new Error(`Missing submission guidance for form id ${formId}`) + } + + try { + // Send confirmation email + await sendNotification({ + templateId, + emailAddress, + personalisation: { + subject, + body: getUserConfirmationEmailBody(formName, new Date(), metadata) + } + }) + + logger.info(logTags, 'User confirmation email sent successfully') + } catch (err) { + const errMsg = getErrorMessage(err) + logger.error( + err, + `[emailSendFailed] Error sending user confirmation email - templateId: ${templateId} - ${errMsg}` + ) + + throw err + } +} + /** * @import { FormAdapterSubmissionMessage } from '@defra/forms-engine-plugin/engine/types.js' */ diff --git a/src/service/notify.test.js b/src/service/notify.test.js index e402491..a481412 100644 --- a/src/service/notify.test.js +++ b/src/service/notify.test.js @@ -15,7 +15,7 @@ import { buildFormAdapterSubmissionMessageMetaStub, buildFormAdapterSubmissionMessageResult } from '~/src/service/__stubs__/event-builders.js' -import { sendNotifyEmail } from '~/src/service/notify.js' +import { sendNotifyEmails } from '~/src/service/notify.js' jest.mock('~/src/helpers/logging/logger.js', () => ({ createLogger: () => ({ @@ -113,7 +113,7 @@ describe('notify', () => { lists: [] }) - describe('sendNotifyEmail', () => { + describe('sendNotifyEmails', () => { it('should send a v1 machine readable email', async () => { const definition = buildDefinition({ ...baseDefinition, @@ -123,7 +123,7 @@ describe('notify', () => { } }) jest.mocked(getFormDefinition).mockResolvedValueOnce(definition) - await sendNotifyEmail(formAdapterSubmissionMessage) + await sendNotifyEmails(formAdapterSubmissionMessage) const [sendNotificationCall] = jest.mocked(sendNotification).mock.calls[0] expect(sendNotificationCall).toEqual({ @@ -155,7 +155,7 @@ describe('notify', () => { it('should send a v2 machine readable email', async () => { jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) - await sendNotifyEmail(formAdapterSubmissionMessage) + await sendNotifyEmails(formAdapterSubmissionMessage) const [sendNotificationCall] = jest.mocked(sendNotification).mock.calls[0] expect(sendNotificationCall).toEqual({ @@ -627,7 +627,7 @@ describe('notify', () => { }) jest.mocked(getFormDefinition).mockResolvedValueOnce(definition) - await sendNotifyEmail(formAdapterSubmissionMessage) + await sendNotifyEmails(formAdapterSubmissionMessage) expect(getFormDefinition).toHaveBeenCalledWith( formId, FormStatus.Live, @@ -648,7 +648,7 @@ describe('notify', () => { jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) jest.mocked(sendNotification).mockRejectedValueOnce(err) await expect( - sendNotifyEmail(formAdapterSubmissionMessage) + sendNotifyEmails(formAdapterSubmissionMessage) ).rejects.toThrow(err) }) @@ -677,7 +677,7 @@ describe('notify', () => { }) jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) - await sendNotifyEmail(versionedFormAdapterSubmissionMessage) + await sendNotifyEmails(versionedFormAdapterSubmissionMessage) expect(getFormDefinition).toHaveBeenCalledWith( formId, @@ -688,7 +688,7 @@ describe('notify', () => { it('should use default form definition when versionMetadata is not present', async () => { jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) - await sendNotifyEmail(formAdapterSubmissionMessage) + await sendNotifyEmails(formAdapterSubmissionMessage) expect(getFormDefinition).toHaveBeenCalledWith( formId, From 779fbe816477396c174a86d8c75a7f3eb348f62e Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 8 Oct 2025 09:59:00 +0100 Subject: [PATCH 02/12] Extra coverage --- src/lib/manager.test.js | 207 ++++++++++-------- src/service/mappers/user-confirmation.js | 3 +- src/service/mappers/user-confirmation.test.js | 70 ++++++ 3 files changed, 184 insertions(+), 96 deletions(-) create mode 100644 src/service/mappers/user-confirmation.test.js diff --git a/src/lib/manager.test.js b/src/lib/manager.test.js index bc46d24..fff2f59 100644 --- a/src/lib/manager.test.js +++ b/src/lib/manager.test.js @@ -1,8 +1,8 @@ import { FormStatus } from '@defra/forms-model' -import { buildDefinition } from '@defra/forms-model/stubs' +import { buildDefinition, buildMetaData } from '@defra/forms-model/stubs' import { getJson } from '~/src/lib/fetch.js' -import { getFormDefinition } from '~/src/lib/manager.js' +import { getFormDefinition, getFormMetadata } from '~/src/lib/manager.js' jest.mock('~/src/lib/fetch.js') jest.mock('~/src/config/index.js', () => ({ config: { @@ -10,102 +10,121 @@ jest.mock('~/src/config/index.js', () => ({ } })) -describe('getDefinition', () => { - it('should get the current definition if draft', async () => { - const expectedDefinition = buildDefinition() - const formId = '68a890909ab460290c289409' - jest - .mocked(getJson) - .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) - const definition = await getFormDefinition( - formId, - FormStatus.Draft, - undefined - ) - expect(getJson).toHaveBeenCalledWith( - expect.objectContaining({ - href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/draft' - }) - ) - expect(definition).toEqual(expectedDefinition) - }) +describe('Manager', () => { + describe('getDefinition', () => { + it('should get the current definition if draft', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Draft, + undefined + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/draft' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) - it('should get the current definition if live', async () => { - const expectedDefinition = buildDefinition() - const formId = '68a890909ab460290c289409' - jest - .mocked(getJson) - .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) - const definition = await getFormDefinition( - formId, - FormStatus.Live, - undefined - ) - expect(getJson).toHaveBeenCalledWith( - expect.objectContaining({ - href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/' - }) - ) - expect(definition).toEqual(expectedDefinition) - }) + it('should get the current definition if live', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Live, + undefined + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) - it('should get a specific version if version number is provided', async () => { - const expectedDefinition = buildDefinition() - const formId = '68a890909ab460290c289409' - const versionNumber = 3 - jest - .mocked(getJson) - .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) - const definition = await getFormDefinition( - formId, - FormStatus.Live, - versionNumber - ) - expect(getJson).toHaveBeenCalledWith( - expect.objectContaining({ - href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/3/definition' - }) - ) - expect(definition).toEqual(expectedDefinition) - }) + it('should get a specific version if version number is provided', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + const versionNumber = 3 + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Live, + versionNumber + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/3/definition' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) + + it('should get a specific version with draft status if version number is provided', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + const versionNumber = 1 + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Draft, + versionNumber + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/1/definition' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) - it('should get a specific version with draft status if version number is provided', async () => { - const expectedDefinition = buildDefinition() - const formId = '68a890909ab460290c289409' - const versionNumber = 1 - jest - .mocked(getJson) - .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) - const definition = await getFormDefinition( - formId, - FormStatus.Draft, - versionNumber - ) - expect(getJson).toHaveBeenCalledWith( - expect.objectContaining({ - href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/1/definition' - }) - ) - expect(definition).toEqual(expectedDefinition) + it('should handle version number 0 correctly', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + const versionNumber = 0 + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Live, + versionNumber + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/0/definition' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) }) - it('should handle version number 0 correctly', async () => { - const expectedDefinition = buildDefinition() - const formId = '68a890909ab460290c289409' - const versionNumber = 0 - jest - .mocked(getJson) - .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) - const definition = await getFormDefinition( - formId, - FormStatus.Live, - versionNumber - ) - expect(getJson).toHaveBeenCalledWith( - expect.objectContaining({ - href: 'http://forms-manager/forms/68a890909ab460290c289409/versions/0/definition' - }) - ) - expect(definition).toEqual(expectedDefinition) + describe('getMetadata', () => { + it('should get the metadata', async () => { + const expectedMetadata = buildMetaData() + const formId = '68a890909ab460290c289409' + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedMetadata }) + const metadata = await getFormMetadata(formId) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409' + }) + ) + expect(metadata).toEqual(expectedMetadata) + }) }) }) diff --git a/src/service/mappers/user-confirmation.js b/src/service/mappers/user-confirmation.js index 37d2603..a8f83c5 100644 --- a/src/service/mappers/user-confirmation.js +++ b/src/service/mappers/user-confirmation.js @@ -33,8 +33,7 @@ We received your form submission for “${formName}’ on ${formattedSub ${submissionGuidance} ## Get help -${contactDetails} -Do not reply to this emall. We do not monitor reples to this email address. +${contactDetails}Do not reply to this emall. We do not monitor reples to this email address. From ${escapeMarkdown(organisation)} ` diff --git a/src/service/mappers/user-confirmation.test.js b/src/service/mappers/user-confirmation.test.js new file mode 100644 index 0000000..4353b01 --- /dev/null +++ b/src/service/mappers/user-confirmation.test.js @@ -0,0 +1,70 @@ +import { buildMetaData } from '@defra/forms-model/stubs' + +import { getUserConfirmationEmailBody } from '~/src/service/mappers/user-confirmation.js' + +describe('user-confirmation', () => { + test('should handle general email content', () => { + const formName = 'My Form Name' + const submissionDate = new Date(2025, 5, 4, 14, 21, 35) + const metadata = buildMetaData({ + submissionGuidance: 'Some submission guidance' + }) + expect( + getUserConfirmationEmailBody(formName, submissionDate, metadata) + ).toBe( + ` +# We have your form +We received your form submission for “My Form Name’ on 2:21pm on Wednesday 4 June 2025. + +## What happens next +Some submission guidance + +## Get help +Do not reply to this emall. We do not monitor reples to this email address. + +From Defra +` + ) + }) + + test('should handle contact details', () => { + const formName = 'My Form Name' + const submissionDate = new Date(2025, 5, 4, 14, 21, 35) + const metadata = buildMetaData({ + submissionGuidance: 'Some submission guidance', + contact: { + phone: '0121 123456789', + email: { + address: 'our-email@test.com', + responseTime: 'We will respond within 5 working days' + }, + online: { + url: 'https://some-online-help.com', + text: 'This is our online url' + } + } + }) + expect( + getUserConfirmationEmailBody(formName, submissionDate, metadata) + ).toBe( + ` +# We have your form +We received your form submission for “My Form Name’ on 2:21pm on Wednesday 4 June 2025. + +## What happens next +Some submission guidance + +## Get help +0121 123456789 + +our-email@test.com We will respond within 5 working days + +https://some-online-help.com This is our online url + +Do not reply to this emall. We do not monitor reples to this email address. + +From Defra +` + ) + }) +}) From 226f718d52a08d31088703f9e64f66f50b829a9a Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 8 Oct 2025 16:28:45 +0100 Subject: [PATCH 03/12] FInal email body changes Allow dynamic properties in 'meta' --- package-lock.json | 18 +++---- package.json | 4 +- src/service/events.js | 16 +++++- src/service/mappers/user-confirmation.js | 6 +-- src/service/mappers/user-confirmation.test.js | 9 ++-- src/service/notify.js | 52 ++++++++++++++----- 6 files changed, 74 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54c5fdd..c653ba1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "license": "OGL-UK-3.0", "dependencies": { "@aws-sdk/client-sqs": "3.894.0", - "@defra/forms-engine-plugin": "3.0.4", - "@defra/forms-model": "3.0.552", + "@defra/forms-engine-plugin": "3.0.7", + "@defra/forms-model": "3.0.559", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/hapi": "^21.4.2", @@ -2308,13 +2308,13 @@ "license": "MIT" }, "node_modules/@defra/forms-engine-plugin": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-3.0.4.tgz", - "integrity": "sha512-aPCE3M22achepzyJum8Su1CdQpyTw71AdD5ee878PL8/Xf1EWceUdSyykOpdbBTQGQHIia0lv9wNqbMjF5d4mg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-3.0.7.tgz", + "integrity": "sha512-PoxCa+V+fosjD6Y3lBL8Fl/mgwL/RDo7caiapbg9dT8DozotW2qo8ulfzTNWuK46tzyGZaDwArUJzOZCuKxCqA==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.552", + "@defra/forms-model": "^3.0.555", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", @@ -2439,9 +2439,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.552", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.552.tgz", - "integrity": "sha512-g2ZpUHL+CQyJbGX1kBpM7pXy33GVKMaS/95it0tJXkkNECO+0pkBdvu2EVrUD6GMHAirvWzhvFVA0y1CX8rP3g==", + "version": "3.0.559", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.559.tgz", + "integrity": "sha512-dSMrTnhUXnapflHKdeQLMGDwK2QlFhp/08XwzLNHzLHmgx7pqHAgelzVeRsyHtzYDu7B7tF4r5cyR+SxI4UmXw==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", diff --git a/package.json b/package.json index d7d5932..a3b796e 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ }, "dependencies": { "@aws-sdk/client-sqs": "3.894.0", - "@defra/forms-engine-plugin": "3.0.4", - "@defra/forms-model": "3.0.552", + "@defra/forms-engine-plugin": "3.0.7", + "@defra/forms-model": "3.0.559", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/hapi": "^21.4.2", diff --git a/src/service/events.js b/src/service/events.js index 421cdbe..2c2eb5b 100644 --- a/src/service/events.js +++ b/src/service/events.js @@ -1,4 +1,7 @@ -import { formAdapterSubmissionMessagePayloadSchema } from '@defra/forms-engine-plugin/engine/types/schema.js' +import { + formAdapterSubmissionMessageMetaSchema, + formAdapterSubmissionMessagePayloadSchema +} from '@defra/forms-engine-plugin/engine/types/schema.js' import { getErrorMessage } from '@defra/forms-model' import Joi from 'joi' @@ -34,6 +37,17 @@ export function mapFormAdapterSubmissionEvent(message) { } ) + // To allow custom properties in 'meta', ensure they don't get stripped + const metaOnlyValue = Joi.attempt( + messageBody.meta, + formAdapterSubmissionMessageMetaSchema, + { + abortEarly: false, + stripUnknown: false + } + ) + value.meta = metaOnlyValue + return { messageId: message.MessageId, ...value, diff --git a/src/service/mappers/user-confirmation.js b/src/service/mappers/user-confirmation.js index a8f83c5..d2172c0 100644 --- a/src/service/mappers/user-confirmation.js +++ b/src/service/mappers/user-confirmation.js @@ -18,16 +18,16 @@ export function getUserConfirmationEmailBody( const phoneDetails = contact?.phone ? `${contact.phone}\n\n` : '' const emailDetails = contact?.email - ? `${contact.email.address} ${contact.email.responseTime}\n\n` + ? `[${contact.email.address}](mailto:${contact.email.address})\n${contact.email.responseTime}\n\n` : '' const onlineDetails = contact?.online - ? `${contact.online.url} ${contact.online.text}\n\n` + ? `[${contact.online.text}](${contact.online.url})\n\n` : '' const contactDetails = `${phoneDetails}${emailDetails}${onlineDetails}` return ` # We have your form -We received your form submission for “${formName}’ on ${formattedSubmissionDate}. +We received your form submission for ‘${formName}’ on ${formattedSubmissionDate}. ## What happens next ${submissionGuidance} diff --git a/src/service/mappers/user-confirmation.test.js b/src/service/mappers/user-confirmation.test.js index 4353b01..11fc5bb 100644 --- a/src/service/mappers/user-confirmation.test.js +++ b/src/service/mappers/user-confirmation.test.js @@ -14,7 +14,7 @@ describe('user-confirmation', () => { ).toBe( ` # We have your form -We received your form submission for “My Form Name’ on 2:21pm on Wednesday 4 June 2025. +We received your form submission for ‘My Form Name’ on 2:21pm on Wednesday 4 June 2025. ## What happens next Some submission guidance @@ -49,7 +49,7 @@ From Defra ).toBe( ` # We have your form -We received your form submission for “My Form Name’ on 2:21pm on Wednesday 4 June 2025. +We received your form submission for ‘My Form Name’ on 2:21pm on Wednesday 4 June 2025. ## What happens next Some submission guidance @@ -57,9 +57,10 @@ Some submission guidance ## Get help 0121 123456789 -our-email@test.com We will respond within 5 working days +[our-email@test.com](mailto:our-email@test.com) +We will respond within 5 working days -https://some-online-help.com This is our online url +[This is our online url](https://some-online-help.com) Do not reply to this emall. We do not monitor reples to this email address. diff --git a/src/service/notify.js b/src/service/notify.js index 00eeda1..7a9bb3c 100644 --- a/src/service/notify.js +++ b/src/service/notify.js @@ -1,10 +1,9 @@ import { escapeMarkdown } from '@defra/forms-engine-plugin/engine/components/helpers/index.js' -import { getFormMetadata } from '@defra/forms-engine-plugin/services/formsService.js' -import { getErrorMessage } from '@defra/forms-model' +import { ControllerType, getErrorMessage } from '@defra/forms-model' import { config } from '~/src/config/index.js' import { createLogger } from '~/src/helpers/logging/logger.js' -import { getFormDefinition } from '~/src/lib/manager.js' +import { getFormDefinition, getFormMetadata } from '~/src/lib/manager.js' import { sendNotification } from '~/src/lib/notify.js' import { getFormatter } from '~/src/service/mappers/formatters/index.js' import { getUserConfirmationEmailBody } from '~/src/service/mappers/user-confirmation.js' @@ -14,13 +13,38 @@ const templateId = /** @type {string} */ (config.get('notifyTemplateId')) const logger = createLogger() +// TODO - need a better way to handle custom controllers in the output formatters +/** + * Revert any custom controllers to their parent/base class since engine-plugin has no knowledge of them + * @param {FormDefinition} definition + * @returns {FormDefinition} + */ +export function removeCustomControllers(definition) { + return { + ...definition, + pages: definition.pages.map((page) => { + if (page.controller) { + const controller = [ + 'SummaryPageWithConfirmationEmailController' + ].includes(page.controller) + ? ControllerType.Summary + : page.controller + return /** @type {Page} */ ({ + ...page, + controller + }) + } + return page + }) + } +} + /** * Sends a mail to notify * @param {FormAdapterSubmissionMessage} formSubmissionMessage * @returns {Promise} */ export async function sendNotifyEmails(formSubmissionMessage) { - // TODO - decide how to handle if first email throws await sendInternalEmail(formSubmissionMessage) await sendUserConfirmationEmail(formSubmissionMessage) } @@ -51,12 +75,14 @@ export async function sendInternalEmail(formSubmissionMessage) { `Getting form definition: ${formId} version: ${versionMetadata?.versionNumber} - internal submission email` ) - const definition = await getFormDefinition( + const origDefinition = await getFormDefinition( formId, status, versionMetadata?.versionNumber ) + const definition = removeCustomControllers(origDefinition) + const formName = escapeMarkdown(formNameInput) const subject = isPreview ? `TEST FORM SUBMISSION: ${formName}` @@ -105,22 +131,23 @@ export async function sendInternalEmail(formSubmissionMessage) { * @returns {Promise} */ export async function sendUserConfirmationEmail(formSubmissionMessage) { - if (!formSubmissionMessage.meta.userConfirmationEmail) { - return - } - const { formId, formName: formNameInput, - isPreview + isPreview, + userConfirmationEmail } = formSubmissionMessage.meta + + if (!userConfirmationEmail) { + return + } + const logTags = ['submit', 'email'] // Get submission email personalisation logger.info(logTags, 'Getting personalisation data - user confirmation email') const formName = escapeMarkdown(formNameInput) - const emailAddress = formSubmissionMessage.meta.userConfirmationEmail const metadata = await getFormMetadata(formId) @@ -138,7 +165,7 @@ export async function sendUserConfirmationEmail(formSubmissionMessage) { // Send confirmation email await sendNotification({ templateId, - emailAddress, + emailAddress: /** @type {string} */ (userConfirmationEmail), personalisation: { subject, body: getUserConfirmationEmailBody(formName, new Date(), metadata) @@ -158,5 +185,6 @@ export async function sendUserConfirmationEmail(formSubmissionMessage) { } /** + * @import { FormDefinition, Page } from '@defra/forms-model' * @import { FormAdapterSubmissionMessage } from '@defra/forms-engine-plugin/engine/types.js' */ From b40126031699ec8c9b59ef3b5a30345b840366f8 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 8 Oct 2025 17:30:11 +0100 Subject: [PATCH 04/12] Changed to use custom property in meta Added test for confirmation emails --- src/service/notify.js | 4 ++- src/service/notify.test.js | 56 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/service/notify.js b/src/service/notify.js index 7a9bb3c..10449b2 100644 --- a/src/service/notify.js +++ b/src/service/notify.js @@ -135,9 +135,11 @@ export async function sendUserConfirmationEmail(formSubmissionMessage) { formId, formName: formNameInput, isPreview, - userConfirmationEmail + custom } = formSubmissionMessage.meta + const userConfirmationEmail = custom?.userConfirmationEmail + if (!userConfirmationEmail) { return } diff --git a/src/service/notify.test.js b/src/service/notify.test.js index a481412..6e56cb4 100644 --- a/src/service/notify.test.js +++ b/src/service/notify.test.js @@ -5,9 +5,9 @@ import { FormStatus, SchemaVersion } from '@defra/forms-model' -import { buildDefinition } from '@defra/forms-model/stubs' +import { buildDefinition, buildMetaData } from '@defra/forms-model/stubs' -import { getFormDefinition } from '~/src/lib/manager.js' +import { getFormDefinition, getFormMetadata } from '~/src/lib/manager.js' import { sendNotification } from '~/src/lib/notify.js' import { buildFormAdapterSubmissionMessage, @@ -643,6 +643,58 @@ describe('notify', () => { }) }) + it('should send a user confirmation email', async () => { + jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) + jest.mocked(getFormMetadata).mockResolvedValueOnce( + buildMetaData({ + submissionGuidance: 'Some guidance text' + }) + ) + const formAdapterMessageWithUserEmail = structuredClone( + formAdapterSubmissionMessage + ) + formAdapterMessageWithUserEmail.meta.custom = { + userConfirmationEmail: 'my-email@test.com' + } + await sendNotifyEmails(formAdapterMessageWithUserEmail) + + expect(jest.mocked(sendNotification)).toHaveBeenCalledTimes(2) + const [sendNotificationCall] = jest.mocked(sendNotification).mock.calls[0] + expect(sendNotificationCall).toEqual({ + templateId: 'notify-template-id-1', + emailAddress: 'notificationEmail@example.uk', + personalisation: { + subject: 'Form submission: Machine readable form', + body: expect.any(String) + } + }) + const [sendConfirmationCall] = jest.mocked(sendNotification).mock.calls[1] + expect(sendConfirmationCall).toEqual({ + templateId: 'notify-template-id-1', + emailAddress: 'my-email@test.com', + personalisation: { + subject: 'Form submitted to Defra', + body: expect.any(String) + } + }) + const sendNotificationBody = JSON.parse( + Buffer.from( + sendNotificationCall.personalisation.body, + 'base64' + ).toString('utf-8') + ) + expect(new Date(sendNotificationBody.meta.timestamp)).not.toBeNaN() + expect(sendNotificationBody).toEqual({ + meta: { + schemaVersion: '2', + timestamp: expect.any(String), + referenceNumber, + definition: baseDefinition + }, + data: formSubmissionData + }) + }) + it('should handle and throw errors', async () => { const err = new Error('Upstream failure') jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) From 0a6d86b4782d10dfd6f0e6dee683bd2e721d708c Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Wed, 8 Oct 2025 17:38:09 +0100 Subject: [PATCH 05/12] Reverted --- src/service/events.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/service/events.js b/src/service/events.js index 2c2eb5b..421cdbe 100644 --- a/src/service/events.js +++ b/src/service/events.js @@ -1,7 +1,4 @@ -import { - formAdapterSubmissionMessageMetaSchema, - formAdapterSubmissionMessagePayloadSchema -} from '@defra/forms-engine-plugin/engine/types/schema.js' +import { formAdapterSubmissionMessagePayloadSchema } from '@defra/forms-engine-plugin/engine/types/schema.js' import { getErrorMessage } from '@defra/forms-model' import Joi from 'joi' @@ -37,17 +34,6 @@ export function mapFormAdapterSubmissionEvent(message) { } ) - // To allow custom properties in 'meta', ensure they don't get stripped - const metaOnlyValue = Joi.attempt( - messageBody.meta, - formAdapterSubmissionMessageMetaSchema, - { - abortEarly: false, - stripUnknown: false - } - ) - value.meta = metaOnlyValue - return { messageId: message.MessageId, ...value, From a261cd93b01cc518a0f9401c4513b63ce72d68bc Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 10:22:42 +0100 Subject: [PATCH 06/12] Upversioned engine plugin --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c653ba1..9a67e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "OGL-UK-3.0", "dependencies": { "@aws-sdk/client-sqs": "3.894.0", - "@defra/forms-engine-plugin": "3.0.7", + "@defra/forms-engine-plugin": "3.0.9", "@defra/forms-model": "3.0.559", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -2308,13 +2308,13 @@ "license": "MIT" }, "node_modules/@defra/forms-engine-plugin": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-3.0.7.tgz", - "integrity": "sha512-PoxCa+V+fosjD6Y3lBL8Fl/mgwL/RDo7caiapbg9dT8DozotW2qo8ulfzTNWuK46tzyGZaDwArUJzOZCuKxCqA==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-3.0.9.tgz", + "integrity": "sha512-RRAycdYqBwjoPqDwawIEzSab2QZjtl8wtvQPvurKGDRD2ZMbAZg44LKd3X9RKRW5eztdLs+deGgJkBPVdSVhdw==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.555", + "@defra/forms-model": "^3.0.559", "@defra/hapi-tracing": "^1.26.0", "@elastic/ecs-pino-format": "^1.5.0", "@hapi/boom": "^10.0.1", diff --git a/package.json b/package.json index a3b796e..51cbdc2 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ }, "dependencies": { "@aws-sdk/client-sqs": "3.894.0", - "@defra/forms-engine-plugin": "3.0.7", + "@defra/forms-engine-plugin": "3.0.9", "@defra/forms-model": "3.0.559", "@defra/hapi-tracing": "^1.28.0", "@elastic/ecs-pino-format": "^1.5.0", From c1ee5b99b07e860fcbc0988fe5c8c51ad46e83cb Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 10:47:47 +0100 Subject: [PATCH 07/12] Resolved date in tests being shifted by BST --- src/service/mappers/user-confirmation.test.js | 8 ++++---- src/service/notify.js | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/service/mappers/user-confirmation.test.js b/src/service/mappers/user-confirmation.test.js index 11fc5bb..727a517 100644 --- a/src/service/mappers/user-confirmation.test.js +++ b/src/service/mappers/user-confirmation.test.js @@ -5,7 +5,7 @@ import { getUserConfirmationEmailBody } from '~/src/service/mappers/user-confirm describe('user-confirmation', () => { test('should handle general email content', () => { const formName = 'My Form Name' - const submissionDate = new Date(2025, 5, 4, 14, 21, 35) + const submissionDate = new Date(2025, 10, 4, 14, 21, 35) // Novemeber date to prevent issues with BST during testing const metadata = buildMetaData({ submissionGuidance: 'Some submission guidance' }) @@ -14,7 +14,7 @@ describe('user-confirmation', () => { ).toBe( ` # We have your form -We received your form submission for ‘My Form Name’ on 2:21pm on Wednesday 4 June 2025. +We received your form submission for ‘My Form Name’ on 2:21pm on Tuesday 4 November 2025. ## What happens next Some submission guidance @@ -29,7 +29,7 @@ From Defra test('should handle contact details', () => { const formName = 'My Form Name' - const submissionDate = new Date(2025, 5, 4, 14, 21, 35) + const submissionDate = new Date(2025, 10, 4, 14, 21, 35) // Novemeber date to prevent issues with BST during testing const metadata = buildMetaData({ submissionGuidance: 'Some submission guidance', contact: { @@ -49,7 +49,7 @@ From Defra ).toBe( ` # We have your form -We received your form submission for ‘My Form Name’ on 2:21pm on Wednesday 4 June 2025. +We received your form submission for ‘My Form Name’ on 2:21pm on Tuesday 4 November 2025. ## What happens next Some submission guidance diff --git a/src/service/notify.js b/src/service/notify.js index 10449b2..ae32d6d 100644 --- a/src/service/notify.js +++ b/src/service/notify.js @@ -141,6 +141,7 @@ export async function sendUserConfirmationEmail(formSubmissionMessage) { const userConfirmationEmail = custom?.userConfirmationEmail if (!userConfirmationEmail) { + // Don't send confirmation email if no email address passed in the message return } From d8a02ab8fd91e03d0690770164c7a2563213b823 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 11:04:17 +0100 Subject: [PATCH 08/12] Extra coverage --- src/lib/manager.test.js | 19 ++++++++++++++++ src/service/notify.test.js | 45 +++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/lib/manager.test.js b/src/lib/manager.test.js index fff2f59..f99e089 100644 --- a/src/lib/manager.test.js +++ b/src/lib/manager.test.js @@ -109,6 +109,25 @@ describe('Manager', () => { ) expect(definition).toEqual(expectedDefinition) }) + + it('should throw if no manager url set', async () => { + const expectedDefinition = buildDefinition() + const formId = '68a890909ab460290c289409' + jest + .mocked(getJson) + .mockResolvedValueOnce({ response: {}, body: expectedDefinition }) + const definition = await getFormDefinition( + formId, + FormStatus.Draft, + undefined + ) + expect(getJson).toHaveBeenCalledWith( + expect.objectContaining({ + href: 'http://forms-manager/forms/68a890909ab460290c289409/definition/draft' + }) + ) + expect(definition).toEqual(expectedDefinition) + }) }) describe('getMetadata', () => { diff --git a/src/service/notify.test.js b/src/service/notify.test.js index 6e56cb4..92ffd48 100644 --- a/src/service/notify.test.js +++ b/src/service/notify.test.js @@ -15,7 +15,10 @@ import { buildFormAdapterSubmissionMessageMetaStub, buildFormAdapterSubmissionMessageResult } from '~/src/service/__stubs__/event-builders.js' -import { sendNotifyEmails } from '~/src/service/notify.js' +import { + sendNotifyEmails, + sendUserConfirmationEmail +} from '~/src/service/notify.js' jest.mock('~/src/helpers/logging/logger.js', () => ({ createLogger: () => ({ @@ -695,6 +698,46 @@ describe('notify', () => { }) }) + it('should throw if confirmation email has no submission guidance set', async () => { + jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) + jest.mocked(getFormMetadata).mockResolvedValueOnce( + buildMetaData({ + submissionGuidance: undefined + }) + ) + const formAdapterMessageWithUserEmail = structuredClone( + formAdapterSubmissionMessage + ) + formAdapterMessageWithUserEmail.meta.custom = { + userConfirmationEmail: 'my-email@test.com' + } + await expect(() => + sendNotifyEmails(formAdapterMessageWithUserEmail) + ).rejects.toThrow( + 'Missing submission guidance for form id 68a8b0449ab460290c28940a' + ) + }) + + it('confirmation email should handle and throw errors', async () => { + const err = new Error('Upstream failure') + jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) + jest.mocked(getFormMetadata).mockResolvedValueOnce( + buildMetaData({ + submissionGuidance: 'Some guidance text' + }) + ) + const formAdapterMessageWithUserEmail = structuredClone( + formAdapterSubmissionMessage + ) + formAdapterMessageWithUserEmail.meta.custom = { + userConfirmationEmail: 'my-email@test.com' + } + jest.mocked(sendNotification).mockRejectedValueOnce(err) + await expect( + sendUserConfirmationEmail(formAdapterMessageWithUserEmail) + ).rejects.toThrow(err) + }) + it('should handle and throw errors', async () => { const err = new Error('Upstream failure') jest.mocked(getFormDefinition).mockResolvedValueOnce(baseDefinition) From 8ab93b72dfb0efc152b4a2528a1bbb942953bc1a Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 12:13:16 +0100 Subject: [PATCH 09/12] Removed coercing --- src/lib/manager.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib/manager.js b/src/lib/manager.js index 0731a15..61310b4 100644 --- a/src/lib/manager.js +++ b/src/lib/manager.js @@ -3,7 +3,7 @@ import { FormStatus } from '@defra/forms-model' import { config } from '~/src/config/index.js' import { getJson } from '~/src/lib/fetch.js' -const managerUrl = config.get('managerUrl') +const managerUrl = /** @type { string | null } */ (config.get('managerUrl')) /** * Gets the form definition from the Forms Manager API∂ * @param {string} formId @@ -12,9 +12,7 @@ const managerUrl = config.get('managerUrl') * @returns {Promise} */ export async function getFormDefinition(formId, formStatus, versionNumber) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!managerUrl) { - // TS / eslint conflict throw new Error('Missing MANAGER_URL') } @@ -38,9 +36,7 @@ export async function getFormDefinition(formId, formStatus, versionNumber) { * @returns {Promise} */ export async function getFormMetadata(formId) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!managerUrl) { - // TS / eslint conflict throw new Error('Missing MANAGER_URL') } From d5244c5866100229ef0d7d0abf82dca9d24184e0 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 12:21:06 +0100 Subject: [PATCH 10/12] Uses ISO string --- src/service/mappers/user-confirmation.test.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/service/mappers/user-confirmation.test.js b/src/service/mappers/user-confirmation.test.js index 727a517..9097b79 100644 --- a/src/service/mappers/user-confirmation.test.js +++ b/src/service/mappers/user-confirmation.test.js @@ -5,7 +5,7 @@ import { getUserConfirmationEmailBody } from '~/src/service/mappers/user-confirm describe('user-confirmation', () => { test('should handle general email content', () => { const formName = 'My Form Name' - const submissionDate = new Date(2025, 10, 4, 14, 21, 35) // Novemeber date to prevent issues with BST during testing + const submissionDate = new Date('2025-11-04T14:21:35+00:00') const metadata = buildMetaData({ submissionGuidance: 'Some submission guidance' }) @@ -27,9 +27,20 @@ From Defra ) }) + test('should handle time shift - plus 1 hour', () => { + const formName = 'My Form Name' + const submissionDate = new Date('2025-11-04T14:21:35+01:00') + const metadata = buildMetaData({ + submissionGuidance: 'Some submission guidance' + }) + expect( + getUserConfirmationEmailBody(formName, submissionDate, metadata) + ).toContain(' on 1:21pm on Tuesday 4 November 2025.') + }) + test('should handle contact details', () => { const formName = 'My Form Name' - const submissionDate = new Date(2025, 10, 4, 14, 21, 35) // Novemeber date to prevent issues with BST during testing + const submissionDate = new Date('2025-11-04T14:21:35+00:00') const metadata = buildMetaData({ submissionGuidance: 'Some submission guidance', contact: { From 33e341f0ee280ba6000ffb782b9d03cff16a9334 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 12:23:57 +0100 Subject: [PATCH 11/12] Added BST test --- src/service/mappers/user-confirmation.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/service/mappers/user-confirmation.test.js b/src/service/mappers/user-confirmation.test.js index 9097b79..dc35b8d 100644 --- a/src/service/mappers/user-confirmation.test.js +++ b/src/service/mappers/user-confirmation.test.js @@ -38,6 +38,17 @@ From Defra ).toContain(' on 1:21pm on Tuesday 4 November 2025.') }) + test('should handle time shift - plus 1 hour in BST', () => { + const formName = 'My Form Name' + const submissionDate = new Date('2025-05-04T14:21:35+01:00') + const metadata = buildMetaData({ + submissionGuidance: 'Some submission guidance' + }) + expect( + getUserConfirmationEmailBody(formName, submissionDate, metadata) + ).toContain(' on 2:21pm on Sunday 4 May 2025.') + }) + test('should handle contact details', () => { const formName = 'My Form Name' const submissionDate = new Date('2025-11-04T14:21:35+00:00') From 859f45f890bbff26f9e3c6b60ffaa567baef37c5 Mon Sep 17 00:00:00 2001 From: Jez Barnsley Date: Thu, 9 Oct 2025 12:35:08 +0100 Subject: [PATCH 12/12] CHanged managerUrl config --- src/config/index.js | 4 ++-- src/lib/manager.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 4f78e8b..26aa073 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -127,13 +127,13 @@ export const config = convict({ designerUrl: { doc: 'URL to call Forms Designer', format: String, - default: null, + default: '', env: 'DESIGNER_URL' }, managerUrl: { doc: 'URL to call Forms Manager API', format: String, - default: null, + default: '', env: 'MANAGER_URL' }, isSecureContextEnabled: { diff --git a/src/lib/manager.js b/src/lib/manager.js index 61310b4..d4ed5e3 100644 --- a/src/lib/manager.js +++ b/src/lib/manager.js @@ -3,7 +3,7 @@ import { FormStatus } from '@defra/forms-model' import { config } from '~/src/config/index.js' import { getJson } from '~/src/lib/fetch.js' -const managerUrl = /** @type { string | null } */ (config.get('managerUrl')) +const managerUrl = config.get('managerUrl') /** * Gets the form definition from the Forms Manager API∂ * @param {string} formId