diff --git a/openapi/json-schemas/api/_questionnaires/post/req/201.json b/openapi/json-schemas/api/_questionnaires/post/req/201.json index 9fda570c..b68439b7 100644 --- a/openapi/json-schemas/api/_questionnaires/post/req/201.json +++ b/openapi/json-schemas/api/_questionnaires/post/req/201.json @@ -15,7 +15,15 @@ "attributes": { "type": "object", "additionalProperties": false, - "required": ["templateName", "owner"], + "required": ["owner"], + "oneOf": [ + { + "required": ["templateName"] + }, + { + "required": ["template"] + } + ], "properties": { "templateName": { "$ref": "../../../../models/definitions/questionnaire-template-name.json" @@ -57,20 +65,7 @@ "templateVersion": { "$ref": "../../../../models/definitions/semver.json" }, - "userData": { - "type": "object", - "required": ["personalisation", "caseReference"], - "additionalProperties": false, - "properties": { - "personalisation": { - "type": "object" - }, - "caseReference": { - "type": "string" - } - } - }, - "preMadeQuestionnaire": { + "template": { "type": "object", "required": [ "type", @@ -97,7 +92,8 @@ "taxonomies": {"type": "object"}, "onSubmit": {"type": "object"}, "onCreate": {"type": "object"}, - "meta": {"type": "object"} + "meta": {"type": "object"}, + "inputSchema": {"type": "object"} } } } diff --git a/openapi/json-schemas/api/_questionnaires_letters/delete/req/204.json b/openapi/json-schemas/api/_questionnaires_letters/delete/req/204.json new file mode 100644 index 00000000..72f332cf --- /dev/null +++ b/openapi/json-schemas/api/_questionnaires_letters/delete/req/204.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": ["type", "attributes"], + "properties": { + "type": { + "const": "questionnaires" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": ["questionnaireIds"], + "properties": { + "questionnaireIds": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "format": "uuid" + } + } + } + } + } + } + } +} diff --git a/openapi/json-schemas/api/_questionnaires_letters/post/req/201.json b/openapi/json-schemas/api/_questionnaires_letters/post/req/201.json new file mode 100644 index 00000000..d3f3a752 --- /dev/null +++ b/openapi/json-schemas/api/_questionnaires_letters/post/req/201.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "required": ["data"], + "properties": { + "data": { + "type": "object", + "additionalProperties": false, + "required": ["type", "attributes"], + "properties": { + "type": { + "const": "questionnaires" + }, + "attributes": { + "type": "object", + "additionalProperties": false, + "required": ["owner", "origin", "system", "template"], + "properties": { + "owner": { + "type": "object", + "required": ["id", "isAuthenticated", "contact-preference"], + "properties": { + "id": { + "$ref": "../../../../models/definitions/owner-id.json" + }, + "isAuthenticated": { + "type": "boolean" + }, + "contact-preference": { + "type": "string", + "enum": ["E", "T"] + }, + "email": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "origin": { + "type": "object", + "additionalProperties": false, + "required": ["channel"], + "properties": { + "channel": { + "type": "string", + "pattern": "^[a-z][a-z0-9]{0,19}(?:-[a-z0-9]{1,20}){0,20}$" + } + } + }, + "system": { + "type": "object", + "required": ["expiry-date", "letter-id"], + "properties": { + "expiry-date": { + "type": "string" + }, + "letter-id": { + "type": "string" + }, + "letter-type": { + "type": "string" + }, + "case-reference": { + "type": "string" + }, + "external-id": { + "$ref": "../../../../models/definitions/external-id.json" + } + } + }, + "template": { + "$ref": "../../../resources/template.json" + } + } + } + } + } + } +} diff --git a/openapi/json-schemas/api/_questionnaires_letters/post/res/201.json b/openapi/json-schemas/api/_questionnaires_letters/post/res/201.json new file mode 100644 index 00000000..3a21756a --- /dev/null +++ b/openapi/json-schemas/api/_questionnaires_letters/post/res/201.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Questionnaires document", + "allOf": [ + {"$ref": "../../../json-api-base-document.json"}, + { + "properties": { + "data": { + "type": "object", + "$ref": "../../../resources/questionnaire-resource.json" + } + } + } + ] +} diff --git a/openapi/json-schemas/api/_questionnaires_template_metadata/get/res/200.json b/openapi/json-schemas/api/_questionnaires_template-metadata/get/res/200.json similarity index 100% rename from openapi/json-schemas/api/_questionnaires_template_metadata/get/res/200.json rename to openapi/json-schemas/api/_questionnaires_template-metadata/get/res/200.json diff --git a/openapi/json-schemas/api/_questionnaires_{questionnaireId}_template_metadata/get/res/200.json b/openapi/json-schemas/api/_questionnaires_{questionnaireId}_template-metadata/get/res/200.json similarity index 100% rename from openapi/json-schemas/api/_questionnaires_{questionnaireId}_template_metadata/get/res/200.json rename to openapi/json-schemas/api/_questionnaires_{questionnaireId}_template-metadata/get/res/200.json diff --git a/openapi/json-schemas/api/resources/template.json b/openapi/json-schemas/api/resources/template.json new file mode 100644 index 00000000..e98b6e72 --- /dev/null +++ b/openapi/json-schemas/api/resources/template.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": [ + "type", + "version", + "sections", + "routes", + "answers", + "progress", + "taxonomies", + "meta" + ], + "additionalProperties": false, + "properties": { + "type": { + "$ref": "../../models/definitions/questionnaire-template-name.json" + }, + "version": { + "$ref": "../../models/definitions/semver.json" + }, + "sections": {"type": "object"}, + "routes": {"type": "object"}, + "answers": {"type": "object"}, + "progress": {"type": "array"}, + "taxonomies": {"type": "object"}, + "onSubmit": {"type": "object"}, + "onCreate": {"type": "object"}, + "meta": {"type": "object"}, + "inputSchema": {"type": "object"} + } +} diff --git a/openapi/openapi.json b/openapi/openapi.json index f26b1c30..4f1d46a7 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -84,14 +84,7 @@ "external": { "id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234" }, - "templateVersion": "1.0.0", - "userData": { - "personalisation": { - "first-name": "Test", - "surname": "Test case" - }, - "caseReference": "123456" - } + "templateVersion": "1.0.0" } } } @@ -689,7 +682,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "json-schemas/api/_questionnaires_metadata/get/res/200.json" + "$ref": "json-schemas/api/_questionnaires_template-metadata/get/res/200.json" }, "example": { "data": [ @@ -752,7 +745,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "json-schemas/api/_questionnaires_{questionnaireId}_metadata/get/res/200.json" + "$ref": "json-schemas/api/_questionnaires_{questionnaireId}_template-metadata/get/res/200.json" }, "example": { "data": { @@ -789,6 +782,152 @@ } } } + }, + "/questionnaires/letters": { + "post": { + "tags": ["Questionnaires", "Letters"], + "summary": "Create a new questionnaire from a specified letter template", + "description": "Create a questionnaire for a letter", + "operationId": "createLetters", + "parameters": [ + { + "$ref": "#/components/parameters/apiVersion" + } + ], + "requestBody": { + "description": "Questionnaire template id", + "required": true, + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "./json-schemas/api/_questionnaires_letters/post/req/201.json" + }, + "example": { + "data": { + "type": "questionnaires", + "attributes": { + "template": { + "type": "letter-template", + "version": "1.0.0", + "sections": {}, + "routes": {}, + "answers": {}, + "progress": [], + "taxonomies": {}, + "meta": {} + }, + "owner": { + "owner-id": "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6", + "is-authenticated": true, + "contact-preference": "E", + "email": "example@f81d4fae-7dec-11d0-a765-00a0c91e6bf6.com" + }, + "origin": { + "channel": "dashboard" + }, + "system": { + "letter-id": "f81d4fae-7dec-11d0-a765-123456781234", + "letter-type": "tx45", + "case-reference": "99-999999", + "expiry-date": "2027-01-01", + "external-id": "urn:uuid:f81d4fae-7dec-11d0-a765-123456781234" + } + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "./json-schemas/api/_questionnaires/post/res/201.json" + }, + "example": { + "data": { + "type": "questionnaires", + "id": "p-some-id", + "attributes": { + "id": "08dbd0d2-6cee-49e5-a55f-35b31ac4aa9e", + "type": "questionnaires", + "version": "1.0.0", + "routes": { + "initial": "p-some-id" + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + }, + "delete": { + "tags": ["Questionnaires", "Letters"], + "summary": "Delete questionnaires for a collection of letter ids", + "description": "Delete questionnaires associated with the supplied letter ids", + "operationId": "deleteLetters", + "parameters": [ + { + "$ref": "#/components/parameters/apiVersion" + } + ], + "requestBody": { + "description": "Collection of letter ids to delete", + "required": true, + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "./json-schemas/api/_questionnaires_letters/delete/req/204.json" + }, + "example": { + "data": { + "type": "questionnaires", + "attributes": { + "questionnaireIds": [ + "f81d4fae-7dec-11d0-a765-123456781234", + "f81d4fae-7dec-11d0-a765-123456781235", + "f81d4fae-7dec-11d0-a765-123456781236" + ] + } + } + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "$ref": "#/components/responses/BadRequest" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + } + } } }, "components": { diff --git a/package-lock.json b/package-lock.json index 85879cff..21a3017e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11468,15 +11468,6 @@ "npm": ">=8.5.2" } }, - "node_modules/q-templates-review": { - "version": "1.0.0", - "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-review.git#ad585754b06368953170db51c79c4d7a995138c9", - "license": "ISC", - "engines": { - "node": ">=16.0.0", - "npm": ">=8.5.2" - } - }, "node_modules/q-templates-application-12_4_4": { "name": "q-templates-application", "version": "12.4.4", @@ -11487,6 +11478,15 @@ "npm": ">=8.5.2" } }, + "node_modules/q-templates-review": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/CriminalInjuriesCompensationAuthority/q-templates-review.git#ad585754b06368953170db51c79c4d7a995138c9", + "license": "ISC", + "engines": { + "node": ">=16.0.0", + "npm": ">=8.5.2" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", diff --git a/questionnaire/letters/letters-routes.js b/questionnaire/letters/letters-routes.js new file mode 100644 index 00000000..791bb0d8 --- /dev/null +++ b/questionnaire/letters/letters-routes.js @@ -0,0 +1,49 @@ +'use strict'; + +const express = require('express'); +const {expressjwt: validateJWT} = require('express-jwt'); + +const createLetterService = require('./letters-service'); +const permissions = require('../../middleware/route-permissions'); + +const router = express.Router(); +router.use(validateJWT({secret: process.env.DCS_JWT_SECRET, algorithms: ['HS256']})); + +router + .route('/letters') + .delete(permissions('letters:delete'), async (req, res, next) => { + try { + const {questionnaireIds} = req.params; + const letterService = createLetterService({ + logger: req.log, + apiVersion: req.get('Dcs-Api-Version') + }); + const response = await letterService.updateQuestionnairesExpiresDate(questionnaireIds); + res.status(204).json(response); + } catch (err) { + next(err); + } + }) + .post(permissions('letters:create'), async (req, res, next) => { + try { + const {template, owner, origin, system} = req.body.data.attributes; + + const letterService = createLetterService({ + logger: req.log, + apiVersion: req.get('Dcs-Api-Version'), + ownerId: owner?.['owner-id'] + }); + const response = await letterService.createQuestionnaire({ + template, + owner, + origin, + system + }); + + res.status(201).json(response); + } catch (err) { + next(err); + } + }); + +module.exports = router; diff --git a/questionnaire/letters/letters-service.js b/questionnaire/letters/letters-service.js new file mode 100644 index 00000000..3aa78fbb --- /dev/null +++ b/questionnaire/letters/letters-service.js @@ -0,0 +1,136 @@ +/* eslint-disable no-useless-catch */ + +'use strict'; + +const VError = require('verror'); +const crypto = require('node:crypto'); +const questionnaireResource = require('../resources/questionnaire-resource'); + +const defaults = {}; +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, + createTaskRunner = defaults.createTaskRunner, + taskImplementations = { + sendNotifyMessageToSQS + } +} = {}) { + const db = createQuestionnaireDAL({logger, ownerId}); + + // Check API version + if (apiVersion !== defaults.apiVersion) { + throw new VError( + { + name: 'ApiVersionNotFound' + }, + `API version "${apiVersion}" is not compatible with this release` + ); + } + + // DAL functions + async function getQuestionnaire(questionnaireId) { + return db.getQuestionnaireByOwner(questionnaireId); + } + + async function updateQuestionnaireExpiresDate(questionnaireId, expiresAt) { + return db.updateQuestionnaireExpiresDate(questionnaireId, expiresAt); + } + + // Task List compatibility + function supportsTaskList(questionnaireDefinition) { + return questionnaireDefinition?.routes?.type === 'parallel'; + } + + async function createQuestionnaire({template, owner, origin, system}) { + const questionnaire = { + id: crypto.randomUUID(), + ...template, + answers: { + ...template.answers, + ...(owner !== undefined && {owner}), + ...(origin !== undefined && {origin}), + ...(system !== undefined && {system}) + } + }; + + await db.createQuestionnaire(questionnaire.id, questionnaire); + + 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); + } + } + + const systemData = questionnaire?.answers?.system; + if (systemData?.['expiry-date']) { + const expiresAt = new Date(systemData?.['expiry-date']); + await updateQuestionnaireExpiresDate(questionnaire.id, expiresAt); + } + + return { + data: questionnaireResource({questionnaire}, supportsTaskList(questionnaire)) + }; + } + + async function updateQuestionnairesExpiresDate(questionnaireIds) { + let notFoundCount = 0; + const results = await Promise.all( + questionnaireIds.map(async questionnaireId => { + try { + // Check the questionnaire exists first + await getQuestionnaire(questionnaireId); + // If it exists, try and delete it + const expiresAt = new Date(); + await updateQuestionnaireExpiresDate(questionnaireId, expiresAt); + return 'success'; + } catch (err) { + if (err.name === 'ResourceNotFound') { + // If the questionnaire doesn't exist log it but don't fail it + logger.info(err.message); + notFoundCount += 1; + return 'notFound'; + } + // If any other errors occur they are legitimate DB errors + throw err; + } + }) + ); + logger.info( + `Successfully deleted ${questionnaireIds.length - notFoundCount} out of ${ + questionnaireIds.length + } questionnaires` + ); + return results; + } + + return Object.freeze({ + createQuestionnaire, + updateQuestionnairesExpiresDate + }); +} + +module.exports = createQuestionnaireService; diff --git a/questionnaire/questionnaire-dal.js b/questionnaire/questionnaire-dal.js index a4e62e80..b8dbdd90 100644 --- a/questionnaire/questionnaire-dal.js +++ b/questionnaire/questionnaire-dal.js @@ -385,13 +385,13 @@ function questionnaireDAL(spec) { return result; } - async function updateQuestionnaireExpiresDate(questionnaireId) { + async function updateQuestionnaireExpiresDate(questionnaireId, expiresAt) { let result; try { result = await db.query( - 'UPDATE questionnaire SET expires = current_timestamp WHERE id = $1', - [questionnaireId] + 'UPDATE questionnaire SET expires = COALESCE($2::timestamptz, CURRENT_TIMESTAMP) WHERE id = $1', + [questionnaireId, expiresAt] ); if (result.rowCount === 0) { throw new VError( diff --git a/questionnaire/questionnaire-dal.test.js b/questionnaire/questionnaire-dal.test.js index 8cd8e389..ac7fade0 100644 --- a/questionnaire/questionnaire-dal.test.js +++ b/questionnaire/questionnaire-dal.test.js @@ -515,7 +515,8 @@ describe('questionnaire data access layer', () => { }); describe('updateQuestionnaireExpiresDate', () => { - const query = 'UPDATE questionnaire SET expires = current_timestamp WHERE id = $1'; + const query = + 'UPDATE questionnaire SET expires = COALESCE($2::timestamptz, CURRENT_TIMESTAMP) WHERE id = $1'; it('Should run an update expiry query and filter by questionnaireId', async () => { const questionnaireId = DB_QUERY_SUCCESS_QUESTIONNAIRE_ID; const questionnaireDAL = createQuestionnaireDAL({logger: jest.fn()}); diff --git a/questionnaire/questionnaire-service.js b/questionnaire/questionnaire-service.js index 6af5e6e3..05052a29 100644 --- a/questionnaire/questionnaire-service.js +++ b/questionnaire/questionnaire-service.js @@ -21,16 +21,11 @@ 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, - createTaskRunner = defaults.createTaskRunner + createQuestionnaireDAL = defaults.createQuestionnaireDAL } = {}) { const db = createQuestionnaireDAL({logger, ownerId}); @@ -68,92 +63,53 @@ function createQuestionnaireService({ return false; } - async function createQuestionnaire({ + async function createQuestionnaire( templateName, ownerData, originData, externalData, - templateVersion, - userData, - preMadeQuestionnaire - }) { - let questionnaire; - if (preMadeQuestionnaire === undefined) { - const templateAsJson = await templateService.getTemplateAsJson( - templateName, - templateVersion + templateVersion + ) { + const templateAsJson = await templateService.getTemplateAsJson( + templateName, + templateVersion + ); + const questionnaire = { + id: crypto.randomUUID(), + ...JSON.parse(templateAsJson) + }; + + if (!ownerData) { + throw new VError( + { + name: 'OwnerNotFound' + }, + `Owner data must be defined` ); - questionnaire = { - id: crypto.randomUUID(), - ...JSON.parse(templateAsJson) - }; + } - if (!ownerData) { - throw new VError( - { - name: 'OwnerNotFound' - }, - `Owner data must be defined` - ); + questionnaire.answers = { + owner: { + 'owner-id': ownerData.id, + 'is-authenticated': ownerData.isAuthenticated } + }; - questionnaire.answers = { - owner: { - 'owner-id': ownerData.id, - 'is-authenticated': ownerData.isAuthenticated - } + if (originData) { + questionnaire.answers.origin = { + channel: originData.channel }; - - if (originData) { - questionnaire.answers.origin = { - channel: originData.channel - }; - } - - if (externalData) { - questionnaire.answers.system = { - 'external-id': externalData.id - }; - } - - if (userData) { - questionnaire.meta.personalisation = userData.personalisation; - questionnaire.answers.system['case-reference'] = userData.caseReference; - questionnaire.answers.system['expiry-date'] = - userData.personalisation['expiry-date']; - } - } else { - // TODO: This will likely need some validation to reduce attack risk - // TODO: Not sure if we want to populate any of this with the other parameters. For now assume not - questionnaire = preMadeQuestionnaire; } - 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 (externalData) { + questionnaire.answers.system = { + 'external-id': externalData.id + }; } - if (ownerData?.isAuthenticated) { + await db.createQuestionnaire(questionnaire.id, questionnaire); + + if (ownerData.isAuthenticated) { await updateExpiryForAuthenticatedOwner(questionnaire.id, ownerData.id); } @@ -690,40 +646,6 @@ function createQuestionnaireService({ }; } - async function updateQuestionnaireExpiresDate(questionnaireId) { - return db.updateQuestionnaireExpiresDate(questionnaireId); - } - - async function updateQuestionnairesExpiresDate(questionnaireIds) { - let notFoundCount = 0; - const results = await Promise.all( - questionnaireIds.map(async questionnaireId => { - try { - // Check the questionnaire exists first - await getQuestionnaire(questionnaireId); - // If it exists, try and delete it - await updateQuestionnaireExpiresDate(questionnaireId); - return 'success'; - } catch (err) { - if (err.name === 'ResourceNotFound') { - // If the questionnaire doesn't exist log it but don't fail it - logger.info(err.message); - notFoundCount += 1; - return 'notFound'; - } - // If any other errors occur they are legitimate DB errors - throw err; - } - }) - ); - logger.info( - `Successfully deleted ${questionnaireIds.length - notFoundCount} out of ${ - questionnaireIds.length - } questionnaires` - ); - return results; - } - return Object.freeze({ createQuestionnaire, createAnswers, @@ -739,8 +661,7 @@ function createQuestionnaireService({ updateExpiryForAuthenticatedOwner, getQuestionnaireIdsBySubmissionStatus, getTemplateMetadata, - getTemplateMetadataById, - updateQuestionnairesExpiresDate + getTemplateMetadataById }); } diff --git a/questionnaire/questionnaire-service.test.js b/questionnaire/questionnaire-service.test.js index 2c653348..f381f9a6 100644 --- a/questionnaire/questionnaire-service.test.js +++ b/questionnaire/questionnaire-service.test.js @@ -301,10 +301,10 @@ describe('Questionnaire Service', () => { }); describe('createQuestionnaire', () => { it('Should create a questionnaire', async () => { - const actual = await questionnaireService.createQuestionnaire({ + const actual = await questionnaireService.createQuestionnaire( templateName, ownerData - }); + ); expect(actual.data).toMatchObject({ id: expect.any(String), @@ -317,12 +317,12 @@ describe('Questionnaire Service', () => { const templateName = 'not-a-template'; await expect( - questionnaireService.createQuestionnaire({templateName, ownerData}) + questionnaireService.createQuestionnaire(templateName, ownerData) ).rejects.toThrow('Template "not-a-template" does not exist'); }); it('Should set owner data in the answers', async () => { - await questionnaireService.createQuestionnaire({templateName, ownerData}); + await questionnaireService.createQuestionnaire(templateName, ownerData); expect(mockDalService.createQuestionnaire).toHaveBeenCalledTimes(1); expect(mockDalService.createQuestionnaire).toHaveBeenCalledWith( @@ -339,11 +339,7 @@ describe('Questionnaire Service', () => { }); it('Should set origin data in the answers if it is included', async () => { - await questionnaireService.createQuestionnaire({ - templateName, - ownerData, - originData - }); + await questionnaireService.createQuestionnaire(templateName, ownerData, originData); expect(mockDalService.createQuestionnaire).toHaveBeenCalledTimes(1); expect(mockDalService.createQuestionnaire).toHaveBeenCalledWith( @@ -363,12 +359,12 @@ describe('Questionnaire Service', () => { }); it('Should set external data in the answers if it is included', async () => { - await questionnaireService.createQuestionnaire({ + await questionnaireService.createQuestionnaire( templateName, ownerData, originData, externalData - }); + ); expect(mockDalService.createQuestionnaire).toHaveBeenCalledTimes(1); expect(mockDalService.createQuestionnaire).toHaveBeenCalledWith( @@ -395,7 +391,7 @@ describe('Questionnaire Service', () => { id: ownerId, isAuthenticated: true }; - await questionnaireService.createQuestionnaire({templateName, ownerData}); + await questionnaireService.createQuestionnaire(templateName, ownerData); expect(mockDalService.updateExpiryForAuthenticatedOwner).toHaveBeenCalledTimes(1); expect(mockDalService.createQuestionnaire).toHaveBeenCalledTimes(1); @@ -420,43 +416,9 @@ describe('Questionnaire Service', () => { const ownerData = undefined; await expect( - questionnaireService.createQuestionnaire({templateName, ownerData}) + 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', () => { @@ -837,83 +799,5 @@ describe('Questionnaire Service', () => { ); }); }); - describe('updateQuestionnairesExpiresDate', () => { - const logger = { - info: jest.fn(() => { - return 'Logged from createQuestionnaire test'; - }) - }; - it('should execute the updateQuestionnairesExpiresDate db call for each questionnaireId', async () => { - const questionnaireService = createQuestionnaireService({ - logger, - apiVersion: undefined // Undefined should only occur for DCS Admin API - }); - await questionnaireService.updateQuestionnairesExpiresDate([ - validQuestionnaireId, - validQuestionnaireId - ]); - - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledTimes(2); - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledWith( - validQuestionnaireId - ); - expect(logger.info).toHaveBeenCalledTimes(1); - expect(logger.info).toHaveBeenCalledWith( - 'Successfully deleted 2 out of 2 questionnaires' - ); - }); - - it('should return successfully even if a questionnaireId cannot be found in the DB', async () => { - const questionnaireService = createQuestionnaireService({ - logger, - apiVersion: undefined // Undefined should only occur for DCS Admin API - }); - const response = await questionnaireService.updateQuestionnairesExpiresDate([ - validQuestionnaireId, - invalidQuestionnaireId - ]); - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledTimes(1); - expect(mockDalService.getQuestionnaireByOwner).toHaveBeenCalledTimes(2); - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledWith( - validQuestionnaireId - ); - expect(mockDalService.getQuestionnaireByOwner).toHaveBeenCalledWith( - validQuestionnaireId - ); - expect(mockDalService.getQuestionnaireByOwner).toHaveBeenCalledWith( - invalidQuestionnaireId - ); - expect(response[0]).toEqual('success'); - expect(response[1]).toEqual('notFound'); - expect(logger.info).toHaveBeenCalledTimes(2); - expect(logger.info).toHaveBeenCalledWith( - 'Successfully deleted 1 out of 2 questionnaires' - ); - expect(logger.info).toHaveBeenCalledWith( - 'Questionnaire "11111111-7dec-11d0-a765-00a0c91e6bf6" not found' - ); - }); - - it('should throw an error if a database error occurs', async () => { - const questionnaireService = createQuestionnaireService({ - logger, - apiVersion: undefined // Undefined should only occur for DCS Admin API - }); - await expect( - questionnaireService.updateQuestionnairesExpiresDate([ - validQuestionnaireId, - incompatibleQuestionnaireId - ]) - ).rejects.toThrow('Questionnaire expiry date was not updated successfully'); - - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledTimes(2); - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledWith( - incompatibleQuestionnaireId - ); - expect(mockDalService.updateQuestionnaireExpiresDate).toHaveBeenCalledWith( - validQuestionnaireId - ); - }); - }); }); }); diff --git a/questionnaire/questionnaire/utils/taskRunner/tasks/createStubs/index.js b/questionnaire/questionnaire/utils/taskRunner/tasks/createStubs/index.js index 8583da50..587d7e53 100644 --- a/questionnaire/questionnaire/utils/taskRunner/tasks/createStubs/index.js +++ b/questionnaire/questionnaire/utils/taskRunner/tasks/createStubs/index.js @@ -3,7 +3,7 @@ const crypto = require('node:crypto'); const createQuestionnaireHelper = require('../../../../questionnaire'); -const createQuestionnaireService = require('../../../../../questionnaire-service'); +const createLetterService = require('../../../../../letters/letters-service'); /** * Creates stub templates specified in the questionnaire metadata @@ -18,7 +18,7 @@ async function createStubs({questionnaire, logger, type}) { const fullQuestionnaireHelper = createQuestionnaireHelper({ questionnaireDefinition: questionnaire }); - const questionnaireService = createQuestionnaireService({ + const letterService = createLetterService({ logger, ownerId: fullQuestionnaireHelper.getAnswers().owner['owner-id'] }); @@ -67,10 +67,15 @@ async function createStubs({questionnaire, logger, type}) { // Create the new questionnaire stub.type = 'stub'; - await questionnaireService.createQuestionnaire({ - owner: questionnaire.owner, - preMadeQuestionnaire: stub + + // Owner, origin and system are already part of the stub. + await letterService.createQuestionnaire({ + template: stub, + owner: undefined, + origin: undefined, + system: undefined }); + logger.info( `Stub with id ${stub.id} created for questionnaire with id ${fullQuestionnaireId}` ); diff --git a/questionnaire/routes.js b/questionnaire/routes.js index 6c451d54..a8178ce0 100644 --- a/questionnaire/routes.js +++ b/questionnaire/routes.js @@ -7,6 +7,7 @@ const createQuestionnaireService = require('./questionnaire-service'); const permissions = require('../middleware/route-permissions'); const metadataRouter = require('./metadata/metadata-routes.js'); const submissionsRouter = require('./submissions/submissions-routes.js'); +const lettersRouter = require('./letters/letters-routes.js'); const router = express.Router(); const rxTemplateName = /^[a-zA-Z0-9-]{1,30}$/; @@ -28,28 +29,20 @@ router.route('/').post(permissions('create:questionnaires'), async (req, res, ne throw err; } - const { - templateName, - owner, - origin, - external, - templateVersion, - userData - } = req.body.data.attributes; + const {templateName, owner, origin, external, templateVersion} = req.body.data.attributes; const questionnaireService = createQuestionnaireService({ logger: req.log, apiVersion: req.get('Dcs-Api-Version'), ownerId: owner?.id }); - const response = await questionnaireService.createQuestionnaire({ + const response = await questionnaireService.createQuestionnaire( templateName, - ownerData: owner, - originData: origin, - externalData: external, - templateVersion, - userData - }); + owner, + origin, + external, + templateVersion + ); res.status(201).json(response); } catch (err) { @@ -59,6 +52,7 @@ router.route('/').post(permissions('create:questionnaires'), async (req, res, ne router.use(metadataRouter); router.use(submissionsRouter); +router.use(lettersRouter); router .route('/:questionnaireId/sections/answers') @@ -218,20 +212,4 @@ router } }); -router.route('/delete').post(permissions('admin'), async (req, res, next) => { - try { - const {questionnaireIds} = req.params; - const questionnaireService = createQuestionnaireService({ - logger: req.log, - apiVersion: req.get('Dcs-Api-Version') - }); - const response = await questionnaireService.updateQuestionnairesExpiresDate( - questionnaireIds - ); - res.status(204).json(response); - } catch (err) { - next(err); - } -}); - module.exports = router; diff --git a/questionnaire/routes.test.js b/questionnaire/routes.test.js index 1b03b7ac..394cc318 100644 --- a/questionnaire/routes.test.js +++ b/questionnaire/routes.test.js @@ -17,7 +17,7 @@ beforeEach(() => { describe('Openapi version 2023-05-17 validation', () => { jest.doMock('./questionnaire-service.js', () => { const questionnaireServiceMock = { - createQuestionnaire: jest.fn(({templateName}) => { + createQuestionnaire: jest.fn(templateName => { if (templateName === 'this-does-not-exist') { throw new VError( { @@ -78,13 +78,7 @@ describe('Openapi version 2023-05-17 validation', () => { `Resource /api/questionnaires/${id}/sections/${section}/answers does not exist` ); } - return { - data: { - type: 'answers', - id: 'id', - attributes: 'coerced answers' - } - }; + return 'ok'; }), updateQuestionnaireSubmissionStatus: jest.fn(() => { return 'ok';