diff --git a/package-lock.json b/package-lock.json index 7507f047..00c99b6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1009,13 +1009,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.8.tgz", - "integrity": "sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", + "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.3.6", + "fast-xml-parser": "5.4.1", "tslib": "^2.6.2" }, "engines": { @@ -9876,9 +9876,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.0.tgz", - "integrity": "sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", diff --git a/questionnaire/questionnaire-service.js b/questionnaire/questionnaire-service.js index afac7542..c8fefc89 100644 --- a/questionnaire/questionnaire-service.js +++ b/questionnaire/questionnaire-service.js @@ -21,11 +21,16 @@ defaults.createQuestionnaireDAL = require('./questionnaire-dal'); defaults.apiVersion = '2023-05-17'; +defaults.createTaskRunner = require('./questionnaire/utils/taskRunner'); +const sendNotifyMessageToSQS = require('./questionnaire/utils/taskRunner/tasks/postToNotify'); +const sequential = require('./questionnaire/utils/taskRunner/tasks/sequential'); + function createQuestionnaireService({ logger, apiVersion = defaults.apiVersion, ownerId, - createQuestionnaireDAL = defaults.createQuestionnaireDAL + createQuestionnaireDAL = defaults.createQuestionnaireDAL, + createTaskRunner = defaults.createTaskRunner } = {}) { const db = createQuestionnaireDAL({logger, ownerId}); @@ -109,6 +114,30 @@ function createQuestionnaireService({ await db.createQuestionnaire(questionnaire.id, questionnaire); + const taskImplementations = { + sendNotifyMessageToSQS + }; + + if (questionnaire.onCreate) { + const onCreateTaskDefinition = JSON.parse(JSON.stringify(questionnaire.onCreate)); + const taskRunner = createTaskRunner({ + taskImplementations: { + sequential, + ...taskImplementations + }, + context: { + logger, + questionnaireDef: questionnaire, + type: 'onCreate' + } + }); + try { + await taskRunner.run(onCreateTaskDefinition); + } catch (error) { + logger.info(error); + } + } + if (ownerData.isAuthenticated) { await updateExpiryForAuthenticatedOwner(questionnaire.id, ownerData.id); } diff --git a/questionnaire/questionnaire-service.test.js b/questionnaire/questionnaire-service.test.js index eccea9d2..7b13d788 100644 --- a/questionnaire/questionnaire-service.test.js +++ b/questionnaire/questionnaire-service.test.js @@ -33,6 +33,22 @@ const failedSubmissionStatus = 'FAILED'; const originData = { channel: 'telephone' }; +const onCreateTasks = { + id: 'task0', + type: 'sequential', + retries: 0, + data: [ + { + id: 'task1', + type: 'sendNotifyMessageToSQS', + retries: 0, + data: { + questionnaire: '$.questionnaireDef', + logger: '$.logger' + } + } + ] +}; beforeEach(() => { jest.clearAllMocks(); @@ -209,6 +225,23 @@ jest.doMock('./utils/isQuestionnaireVersionCompatible', () => questionnaireVersi const mockDalService = require('./questionnaire-dal')(); +jest.doMock('./templates/templates', () => ({ + templateService: { + getTemplateAsJson: async (name, version) => { + const realTemplateService = jest.requireActual('./templates/templates'); + const templateAsJson = await realTemplateService.templateService.getTemplateAsJson( + name, + version + ); + const template = JSON.parse(templateAsJson); + return JSON.stringify({ + ...template, + onCreate: onCreateTasks + }); + } + } +})); + const createQuestionnaireService = require('./questionnaire-service'); describe('Questionnaire Service', () => { @@ -227,7 +260,11 @@ describe('Questionnaire Service', () => { }); describe('DCS API Version 2023-05-17', () => { const questionnaireService = createQuestionnaireService({ - logger: () => 'Logged from createQuestionnaire test', + logger: { + info: jest.fn(() => { + return 'Logged from createQuestionnaire test'; + }) + }, apiVersion, ownerId }); @@ -351,6 +388,40 @@ describe('Questionnaire Service', () => { questionnaireService.createQuestionnaire(templatename, ownerData) ).rejects.toThrow('Owner data must be defined'); }); + + it('Should run any onCreate tasks defined in the questionnaire', async () => { + const runMock = jest.fn(() => 'ok!'); + const questionnaireService = createQuestionnaireService({ + logger: () => 'Logged from createQuestionnaire test', + apiVersion, + createTaskRunner: () => { + return {run: runMock}; + } + }); + await questionnaireService.createQuestionnaire(templatename, ownerData); + expect(runMock).toHaveBeenCalledWith(onCreateTasks); + }); + + it('Should log an error but still return if any onCreate tasks fail', async () => { + const failError = new Error('Task failed to run'); + const runMock = jest.fn(() => { + throw failError; + }); + const loggerMock = {info: jest.fn()}; + const questionnaireService = createQuestionnaireService({ + logger: loggerMock, + apiVersion, + createTaskRunner: () => { + return {run: runMock}; + } + }); + + await expect( + questionnaireService.createQuestionnaire(templatename, ownerData) + ).resolves.not.toThrow(); + expect(runMock).toHaveBeenCalledWith(onCreateTasks); + expect(loggerMock.info).toHaveBeenCalledWith(failError); + }); }); describe('getProgressEntries', () => { diff --git a/questionnaire/questionnaire/questionnaire.js b/questionnaire/questionnaire/questionnaire.js index 8ef0dc37..9c8d9c67 100644 --- a/questionnaire/questionnaire/questionnaire.js +++ b/questionnaire/questionnaire/questionnaire.js @@ -251,7 +251,7 @@ function createQuestionnaire({ if (sectionDefinitionVars !== undefined && allowSummary === true) { const resolvedVars = getResolvedVars(sectionId, sectionDefinitionVars); if (resolvedVars.summary) { - if (sectionDefinition.schema.options) { + if (sectionDefinition.schema.options?.ordering) { const sortingInstructions = sectionDefinition.schema.options.ordering; resolvedVars.summary = sortThemedAnswers( resolvedVars.summary, @@ -346,8 +346,8 @@ function createQuestionnaire({ return undefined; } - function getPermittedActions() { - const actions = questionnaireDefinition?.meta?.onComplete?.actions; + function getPermittedActions(type = 'onComplete') { + const actions = questionnaireDefinition?.meta?.[type]?.actions; if (actions) { const answersAndRoles = { diff --git a/questionnaire/questionnaire/questionnaire.test.js b/questionnaire/questionnaire/questionnaire.test.js index 655ecb50..dc7339c2 100644 --- a/questionnaire/questionnaire/questionnaire.test.js +++ b/questionnaire/questionnaire/questionnaire.test.js @@ -1260,4 +1260,43 @@ describe('Questionnaire', () => { }); }); }); + describe('Given both "onComplete" and "onCreate" actions definitions', () => { + it('should return actions of the correct type', () => { + const questionnaire = createQuestionnaire({ + questionnaireDefinition: { + meta: { + onComplete: { + actions: [ + { + type: 'actionA' + }, + { + type: 'actionD' + } + ] + }, + onCreate: { + actions: [ + { + type: 'actionC' + }, + { + type: 'actionB' + } + ] + } + } + } + }); + const completeActions = questionnaire.getPermittedActions('onComplete'); + const completeActionTypes = completeActions.map(action => action.type); + expect(completeActionTypes.length).toEqual(2); + expect(completeActionTypes).toEqual(['actionA', 'actionD']); + + const createActions = questionnaire.getPermittedActions('onCreate'); + const createActionTypes = createActions.map(action => action.type); + expect(createActionTypes.length).toEqual(2); + expect(createActionTypes).toEqual(['actionC', 'actionB']); + }); + }); }); diff --git a/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.js b/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.js index 43a668cd..340e4f3d 100644 --- a/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.js +++ b/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.js @@ -3,12 +3,12 @@ const createSqsService = require('../../../../../../services/sqs/index'); const createQuestionnaireHelper = require('../../../../questionnaire'); -async function sendNotifyMessageToSQS({questionnaire, logger}) { +async function sendNotifyMessageToSQS({questionnaire, logger, type}) { const questionnaireId = questionnaire.id; logger.info(`Sending notify message to SQS for questionnaire with id ${questionnaireId}`); const sqsService = createSqsService({logger}); const questionnaireDef = createQuestionnaireHelper({questionnaireDefinition: questionnaire}); - const permittedActions = questionnaireDef.getPermittedActions(); + const permittedActions = questionnaireDef.getPermittedActions(type); let sqsResponse = {MessageId: 'MessageId'}; await Promise.all( diff --git a/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.test.js b/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.test.js index 80fb5e36..1a159734 100644 --- a/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.test.js +++ b/questionnaire/questionnaire/utils/taskRunner/tasks/postToNotify/index.test.js @@ -68,4 +68,26 @@ describe('Post to Notify task', () => { expect(sendMock).toHaveBeenCalledTimes(1); expect(mockLogger.info).toHaveBeenCalledTimes(1); }); + + it('Should complete onCreate actions if the type is onCreate', async () => { + sendMock = mockSqsService.mockImplementation(() => ({ + send: payload => payload + })); + const data = { + questionnaire: questionnaireWithEmail, + logger: mockLogger, + type: 'onCreate' + }; + const messageResult = await sendNotifyMessageToSQS(data); + expect(sendMock).toHaveBeenCalledTimes(1); + expect(messageResult).toEqual({ + reference: null, + templateId: '00000000-aaaa-0000-aaaa-000000000000', + emailAddress: 'foo@bar.com', + personalisation: { + caseReference: '12/34567', + content: undefined + } + }); + }); }); diff --git a/questionnaire/questionnaire/utils/taskRunner/tasks/test-fixtures/questionnaireCompleteForCheckYourAnswers.json b/questionnaire/questionnaire/utils/taskRunner/tasks/test-fixtures/questionnaireCompleteForCheckYourAnswers.json index c6419020..697fd6f1 100644 --- a/questionnaire/questionnaire/utils/taskRunner/tasks/test-fixtures/questionnaireCompleteForCheckYourAnswers.json +++ b/questionnaire/questionnaire/utils/taskRunner/tasks/test-fixtures/questionnaireCompleteForCheckYourAnswers.json @@ -296,6 +296,22 @@ } ] }, + "onCreate": { + "actions": [ + { + "data": { + "reference": null, + "templateId": "00000000-aaaa-0000-aaaa-000000000000", + "emailAddress": "foo@bar.com", + "personalisation": { + "caseReference": "12/34567" + } + }, + "type": "sendEmail", + "description": "Notification email - applicant:adult" + } + ] + }, "questionnaireDocumentVersion": "4.2.0" }, "type": "apply-for-compensation", diff --git a/questionnaire/routes.test.js b/questionnaire/routes.test.js index 394cc318..5b2f1920 100644 --- a/questionnaire/routes.test.js +++ b/questionnaire/routes.test.js @@ -78,7 +78,13 @@ describe('Openapi version 2023-05-17 validation', () => { `Resource /api/questionnaires/${id}/sections/${section}/answers does not exist` ); } - return 'ok'; + return { + data: { + type: 'answers', + id: 'id', + attributes: 'coerced answers' + } + }; }), updateQuestionnaireSubmissionStatus: jest.fn(() => { return 'ok'; diff --git a/questionnaire/submissions/submissions-service.js b/questionnaire/submissions/submissions-service.js index 85441c34..7a417ad1 100644 --- a/questionnaire/submissions/submissions-service.js +++ b/questionnaire/submissions/submissions-service.js @@ -90,7 +90,8 @@ function createSubmissionService({ }, context: { logger, - questionnaireDef: questionnaireDefinition + questionnaireDef: questionnaireDefinition, + type: 'onComplete' } });